Unable to update hyperlink address - excel

I'm not very familiar with VBA and am stuck on the last step of finishing my code! What this code does is scan every sheet in the workbook for cells with hyperlinks, and then it goes into the hyperlink and prepends a URL onto it. Everything is working - the last Debug.Print here is properly printing out the correct URL. But then it just hangs and gets stuck on "link.Address = currentAddress". No warning, no error message, it just hangs and does nothing until I press Enter, at which point it highlights that line yellow.
I'm at a loss for how currentAddress can print just fine, but can't be set to the link's address? And it only happens for certain links. Here is one that WORKS:
http://localhost:8000/link?owner=lencompass&name=Create%20a%20JIRA%20ticket%20here!&worksheet=test%20sheet&test=true&destination=https://jira01.corp.censored.com:8443/secure/CreateIssue.jspa%3Fpid=14071%26issuetype=1
Here is one that does NOT WORK:
http://localhost:8000/link?owner=lencompass&name=Core%20Dash%20-%20Performance%20by%20Recipient%20Company%20%26%20Function(Last%2012%20months)&worksheet=test%20sheet&test=true&destination=https://censored.corp.censored.com/accounts/1337/insights/880%3FmultiPeers=309694%2C1586%2C10667%2C1441%2C1009%2C1337%2C1035%2C1028%2C3185%2C1815218%2C96622
These links work when I use them in the browser so I know they are valid links.
Here is my entire VBA script:
Sub trackify_links()
Dim I As Integer
' Loop through each sheet in this workbook
For I = 1 To ActiveWorkbook.Worksheets.Count
' loop through each cell in this sheet
Dim rwIndex As Long
Dim colIndex As Long
Dim maxRow As Long
maxRow = Worksheets(I).Cells(Worksheets(I).Rows.Count, 4).End(xlUp).Row
Worksheets("For IAs").Range("E16") = "Looping over " & maxRow & " rows in sheet: " & Worksheets(I).Name
For rwIndex = 1 To maxRow
' only loop up to the max filled-in column on this row
Dim maxColumn As Long
maxColumn = Worksheets(I).Cells(rwIndex, Worksheets(I).Columns.Count).End(xlToLeft).Column
For colIndex = 1 To maxColumn
Dim linkIndex As Long
Dim link As Hyperlink
For linkIndex = 1 To Worksheets(I).Cells(rwIndex, colIndex).Hyperlinks.Count
Set link = Worksheets(I).Cells(rwIndex, colIndex).Hyperlinks(linkIndex)
' only trackify a link if it isn't already
If Left(link.Address, 30) <> "http://localhost:8000/link?" Then
' this is a QA check - i noticed people putting their local machine paths as links here which won't work for anyone else. Output a list of weird links as a warning
If Left(link.Address, 3) = "../" Or Left(link.Address, 2) = "./" Then
Worksheets("For IAs").Range("E19") = "The link in cell (" & Col_Letter(colIndex) & rwIndex & ") in worksheet " & Worksheets(I).Name & " looks like it's a local path. These links will not work and have not been trackified - consider changing them."
Else
Dim currentAddress As String
' in order for the tracking link to properly redirect, there needs to be an "http://" or "https://" protocol at the beginning
If LCase(Left(link.Address, 7)) <> "http://" And LCase(Left(link.Address, 8)) <> "https://" Then
currentAddress = "https://" & link.Address
Else
currentAddress = link.Address
End If
' replace special characters with hex code so the link is not incorrectly parsed
currentAddress = ConvertToHex(currentAddress)
Dim extraParameters As String
extraParameters = "owner=" & ConvertToHex("lencompass") ' indicate this link belongs to lencompass
extraParameters = extraParameters & "&name=" & ConvertToHex(link.TextToDisplay) ' set the name of this link to the excel link's text"
extraParameters = extraParameters & "&worksheet=" & ConvertToHex(Worksheets(I).Name) ' indicate where in the workbook this link was clicked from (if tab format stays the same it basically will tell what kind of person is clicking)
If Worksheets("For IAs").Range("E3") <> "No" Then _
extraParameters = extraParameters & "&test=true" ' indicate this is a testing link if appropriate
' here we wrap the cell's current link into the tracking link, and customize it with some info about where in the workbook this link was clicked
Debug.Print ("currentaddress: " & currentAddress)
currentAddress = "http://localhost:8000/link?" & extraParameters & "&destination=" & currentAddress
Debug.Print (currentAddress)
link.Address = currentAddress
End If
End If
Next linkIndex
Next colIndex
Next rwIndex
Next I
End Sub
Function Col_Letter(lngCol As Long) As String
Dim vArr
vArr = Split(Cells(1, lngCol).Address(True, False), "$")
Col_Letter = vArr(0)
End Function
Function ConvertToHex(str As String) As String
ConvertToHex = Replace(Replace(Replace(Replace(str, "?", "%3F"), "&", "%26"), " ", "%20"), """", "%22")
End Function

Related

Troubleshoot "Unable to get the Buttons property of the Worksheet class"?

I created a 7-sheet Excel spreadsheet as a test-help companion to a popular bridge book by Kantar called "Modern Bridge Defense". The spreadsheet has a fairly large number of form buttons that allow a user to show or hide test answers from each of the seven chapters in the book
I have vba code that shows or hides the answer text, depending on the caption ('show' or 'hide') of the associated button, and a 'show/hide all' button that will show or hide all answers associated with a particular section of that chapter's test.
I also have vba code that initializes all the buttons on all 7 sheets. When the user first opens the spreadsheet, he/she is asked if they are OK with hiding all the answers. If they agree, the initialization routine loops through all 7 sheets, and the code for each sheet loops through all the buttons on that sheet, hiding each answer that isn't already hidden.
All this works fine if I step through the buttons in debug mode, but fails with "Unable to get the Buttons property of the Worksheet class" at some point (not always the same point) when I try to run it full speed.
This behavior seems like it might be some sort of timing/race issue, but I'm having trouble imagining how that could be, as I don't think I'm really tasking my laptop (XPS15 7590 with 32GB Ram, 1TB SSD).
Here is my initialization routine and the function it calls to iterate through the sheet buttons:
Option Explicit
Private Sub Workbook_Open()
Debug.Print ("In Workbook_Open()")
Dim res As VbMsgBoxResult
res = MsgBox("Hide all answers?", vbYesNoCancel, "Kantar Test Initialization")
If res = vbYes Then
res = MsgBox("Caution! This action will hide all answers - Are you SURE you want to do this?", vbYesNoCancel, "Are you SURE?")
If res = vbYes Then
'OK, user is sure about doing this!
Dim sheet As Worksheet
For Each sheet In Application.Sheets
Debug.Print "Initializing worksheet " & sheet.Name
On Error Resume Next
InitializeAllButtons sheet
If Err <> 0 Then
Debug.Print "call to InitializeAllButtons)" & sheet.Name & " failed with " & Err.Description
End If
Next sheet
End If
End If
End Sub
and here's the function that actually 'clicks' the buttons
Sub InitializeAllButtons(sheet As Worksheet)
Dim btn As Excel.Button, addrstr, startcellstr, startrowstr, endrowstr, colstr As String
Dim pos As Integer
Dim startrow As Integer
Dim endrow As Integer
Dim col As Integer
Dim rownum As Integer
With sheet
For Each btn In .Buttons
Debug.Print (vbTab & btn.Name & ", " & btn.Caption)
'11/27/22 rev to set 'all' button captions to 'Hide All', click on all 'Hide' row buttons
If InStr(btn.Caption, "All") > 0 Then
btn.Caption = "Show All"
Debug.Print "btn " & btn.Name & " caption changed to Show All"
Else 'is normal row show/hide button
If btn.Caption = "Hide" Then
RowButtonClick (btn.Name)
End If
End If
Next
End With
End Sub
In response to a question, here is the 'RowButtonClick()' function
Function RowButtonClick(btn_name As String) As Integer
Dim btn As Excel.Button
Dim btn_text As String
Dim cellstr As String
Dim row, col As Integer
Dim textrange As Range
'for multiple row show/hide ops
Dim cellstrlen As Integer
Dim startrowstr, endrowstr, startcolstr As String
Dim startrow As Integer
Dim endrow As Integer
Dim charidx As Integer
'11/24/22 multiple row button names include '_'
charidx = InStr(btn_name, "_")
If charidx > 0 Then
'11/24/22 row addresses may be 1, 2, or 3 digits
GetStartEndRowCol btn_name, startrow, endrow, col
Debug.Print ("RBC just after GSER: start row = " & startrow & ", end row = " & endrow)
Else 'single row: endrow = startrow
cellstr = Mid(btn_name, 9)
startrow = Range(cellstr).row
endrow = startrow
col = Range(cellstr).Column
Debug.Print "RBC: Single row show/hide action"
End If
Set btn = Application.ActiveSheet.Buttons(btn_name)
btn_text = btn.Caption
With Application.ActiveSheet
If btn_text = "Show" Then
Application.ActiveSheet.Range(.Cells(startrow, col + 1), .Cells(endrow, col + 2)).NumberFormat = ""
btn.Caption = "Hide"
Else
Application.ActiveSheet.Range(.Cells(startrow, col + 1), .Cells(endrow, col + 2)).NumberFormat = "; ; ;"
btn.Caption = "Show"
End If
End With
RowButtonClick = endrow 'so calling fcn knows the next row to try
End Function

VBA script to run batch file from excel list, read result file, parse result file and write result to primary excel file

So, before I place my code, I'll explain what I am trying to do, because I can't test the script myself due to what it is supposed to do, effecting what it must do. I know this is a bit odd, but bear with me please.
Once every two weeks or so, we currently run batch files to update a specific tool on all the WS's in our organization.
Yes, we do have tool propagation software, but as this specific tool is extremely important, we don't trust it's distribution to any automated method which have proven in most cases to fail without us being able to understand the reason.
So, I wrote a few simple command batch files which run the installation command, and write the output to a text file which we then manually go through to find which ws's it was installed on, and which it wasn't.
The ws's on which it was not installed are the ws's we know we know due to the failure, that we have additional issues with and we then put all our effort into finding and fixing those issues.
As you can imagine, it's a time consuming effort, and I have decided I want to automate as much as possible of the manual check, in order to know quickly which ws's failed, and the fail code.
I start out with a list of ws names in excel.
For example,
K190ASSn1.domainname
m930eastgate.domainname
n190alka.domainname
n190amsv.domainname
n190amzi.domainname
N190ARME.domainname
N190AVMA.domainname
N190AVNT.domainname
n190chockstest.domainname
N190DLCR.domainname
N190DNBS.domainname
N190edsh.domainname
n190ehma2.domainname
N190EISH.domainname
I wrote my script to do the following:
Read all the ws names from column A into an array.
Loop through the array, and use the Shell function to call an external cmd file which then runs, and writes the result of the run into a TXT file located in a directory on the D drive called "Minstall".
I then read the names of all the files created in that directory into a new array.
I sort both arrays from A to Z (using a script I found online) to get everything in the same order for the next stage.
I then loop through the file names in the 2nd array, and read each file into a text field which I then parse to find the result of the script run.
That result is then written into a third array in the same logical position of the file name I read.
Finally, I re-write the file names back to the worksheet, overwriting what was there, and in the adjacent column, I write the run result from the relevant cell position in the third array.
I will then end up with a file that contains all the data in one visible point (I hope).
At a later stage, I will add a script that will email the relevant team with a list of the ws's they need to deal with (Those with any run result different from zero), and what they need to do. But that's not for the here and now.
Since if I run the code and it works (I hope) it would perform the update, and I do not yet want to do that, what I am really looking for, is additional eyes to go over my code, to see if what I wrote for each action as defined above is correct and will work, and if there is a way to perhaps write what I did, better.
In general, I went over each stage and everything "looks" good.
Anyone willing to assist here ?
Added by request of #CDP1802:
Examples of the two different results that can be found in the text files. One contains a result of zero, meaning that the script worked. The other contains a code of 1603, which is a generic "there's a problem captain but I don't know what it is" response from M$ msiexec.
The spaces between the lines of the text are what appear in the actual text file.
Example 1 (0 response)
PsExec v2.33 - Execute processes remotely
Copyright (C) 2001-2021 Mark Russinovich
Sysinternals - www.sysinternals.com
C:\Windows\system32>msiexec /i "\\server\Minstall\Installation.msi" /qn ACCEPTEULA=YES REPORTSERVER=server.domainname USESSL=TRUE REPORTSERVERPORT=443 IGNORESSLERRORS=TRUE InstallCertificates=yes /l*v C:\Windows\TEMP\install_log4258289.txt
Connecting to K190LPRTLV4.iaadom...
Starting PSEXESVC service on K190LPRTLV4.iaadom...
Copying authentication key to K190LPRTLV4.iaadom...
Connecting with PsExec service on K190LPRTLV4.iaadom...
Copying d:\Install425.bat to K190LPRTLV4.iaadom...
Starting d:\Install425.bat on K190LPRTLV4.iaadom...
Install425.bat exited on K190LPRTLV4.iaadom with error code 0.
Example 2 (1603 response)
PsExec v2.33 - Execute processes remotely
Copyright (C) 2001-2021 Mark Russinovich
Sysinternals - www.sysinternals.com
C:\Windows\system32>msiexec /i "\\server\Minstall\Installation.msi" /qn ACCEPTEULA=YES REPORTSERVER=server.domainname USESSL=TRUE REPORTSERVERPORT=443 IGNORESSLERRORS=TRUE InstallCertificates=yes /l*v C:\Windows\TEMP\install_log4258289.txt
Connecting to K190LPRTLV3.iaadom...
Starting PSEXESVC service on K190LPRTLV3.iaadom...
Copying authentication key to K190LPRTLV3.iaadom...
Connecting with PsExec service on K190LPRTLV3.iaadom...
Copying d:\Install425.bat to K190LPRTLV3.iaadom...
Starting d:\Install425.bat on K190LPRTLV3.iaadom...
Install425.bat exited on K190LPRTLV3.iaadom with error code 1603.
The updated code is as follows:
Option Explicit
Sub Check_Files()
Const Col_Names = "A"
Const Col_Result = "B"
Const Row_Text = 4 'first line of text and result
Dim wb As Workbook
Dim wsMain As Worksheet
Dim WSNames() As String 'Will hold all the ws names as an array read from column A
Dim WSResult() 'Will hold result for specific ws
Dim DirectoryListArray() As string
ReDim DirectoryListArray(3000) 'Set the directory listing array size to 3000 as a max count
Dim NumberArray() As Long
Dim lastrow As Long, FileCount As Long, NumberCount As Long, r As Long, i As Long, j As Long
Dim awsname as string, strDir As string, strPath As string
Dim item as variant
Dim ReadFile As String, text As String, textline As String, RetCode As Integer
Set wb = ActiveWorkbook
With wb
Set wsMain = .Sheets("Main")
End With
'Copy ws names into array for speed
With wsMain
lastrow = .Cells(.Rows.Count, Col_Names).End(xlUp).Row
If lastrow < Row_Text Then
MsgBox "No ws names found in column " & Col_Names, vbCritical
Exit Sub
End If
WSNames = .Cells(1, Col_Names).Resize(lastrow).Value2
ReDim WSResult(1 To lastrow)
End With
'Write how many names were read into array
Cells(1,3) = "Number of names read into array is " & lastrow
'loop through all ws names and run the batch file for each one
For r = Row_Text To UBound(WSNames)
awsname = WSNames(r, 1) 'Read in next ws name from array
Runcmd(awsname)
Next r
'Write how many batch files were run into worksheet
Cells(2,3) = "Number of batch files run is " & r
'count how many text files have been created
strDir = "D:\Minstall"
strPath = strDir & "\*.txt"
'Loop through all the files in the directory by using Dir$ function
MyFile = Dir$(strPath)
Do While MyFile <> ""
DirectoryListArray(FileCount) = MyFile
MyFile = Dir$
FileCount = FileCount + 1
Loop
'Reset the size of the array without losing its values by using Redim Preserve
Redim Preserve DirectoryListArray(FileCount - 1)
'Write how many text files were found
Cells(3,3) = "Number of txt files found is " & FileCount
''Debug.Print writes the results to the Immediate window (press Ctrl + G to view it)
'For FileCount = 0 To UBound(DirectoryListArray)
'Debug.Print DirectoryListArray(FileCount)
'Next FileCount
'Sort the arrays so that we have the same order in both arrays
'Since both arrays should in effect have the same amount of elements
'sorting names array from A to Z
For i = LBound(WSNames) To UBound(WSNames)
For j = i + 1 To UBound(WSNames)
If UCase(WSNames(i,1)) > UCase(WSNames(j,1)) Then
Temp = WSNames(j,1)
WSNames(j,1) = WSNames(i,1)
WSNames(i,1) = Temp
End If
Next j
Next i
'sorting file array from A to Z
For i = LBound(DirectoryListArray) To UBound(DirectoryListArray)
For j = i + 1 To UBound(DirectoryListArray)
If UCase(DirectoryListArray(i,1)) > UCase(DirectoryListArray(j,1)) Then
Temp = DirectoryListArray(j,1)
DirectoryListArray(j,1) = DirectoryListArray(i,1)
DirectoryListArray(i,1) = Temp
End If
Next j
Next i
NumberCount = 0
'Loop through files in directory based on what's in array
For i = LBound(DirectoryListArray) To UBound(DirectoryListArray)
ReadFile = "D:\Minstall" & "\" & DirectoryListArray(NumberCount)
ReadFile = Application.GetOpenFilename()
Open myFile For Input As #1
Do Until EOF(1)
Line Input #1, textline
text = text & textline
Loop
Close #1
RetCode = InStr(text, "with error code ")
NumFound = Mid(text, posLat + 16, 1)
If NumFound > 0 Then
NumFound = Mid(text, posLat + 16, 4)
'Write the number found into the number array
NumberArray(NumberCount) = NumFound
NumberCount = NumberCount + 1
Else
'Write the number found into the number array
NumberArray(NumberCount) = NumFound
NumberCount = NumberCount + 1
End If
Next i
'Write the ws name into the worksheet and write the number found to the cell to the right of the ws name in the worksheet
For i = LBound(WSNames) To UBound(WSNames)
Cells(j, Col_Names) = WSNames(i,1)
Cells(j, Col_Result) = NumberCount(i,1)
j = j + 1
Next i
End Sub
Sub Runcmd(awsname)
Dim PathToBatch as string
'Set the path and batch file with the ws name as a parameter for the batch to run
PathToBatch = "D:\min425.cmd" & " " & awsname
Call Shell(PathToBatch, vbNormalFocus)
End Sub
The main changes are using a FileSystemObject to read the text files, a Regular Expression to extract the error code, and a WScript.Shell object to run the batch file so macro waits for the script to complete. I have commented out the RunCmd line and replaced it with a RunTest that creates a text file so you can test it.
Option Explicit
Sub Check_Files()
Const DIR_OUT = "D:\Minstall"
Const COL_NAMES = "A"
Const COL_RESULTS = "B"
Const COL_TS = "C" ' timestamp
Const COL_ERR = "D" ' Shell errors
Const ROW_START = 4 'first line of text and result
Dim wb As Workbook, ws As Worksheet
Dim rng As Range, arNames, awsname As String
Dim result As String, txtfile As String
Dim i As Long, LastRow As Long, n As Long, r As Long, colour As Long
Dim t0 As Single: t0 = Timer
Set wb = ThisWorkbook
Set ws = wb.Sheets("Main")
With ws
' read names into array
LastRow = .Cells(.Rows.Count, COL_NAMES).End(xlUp).Row
n = LastRow - ROW_START + 1
If n < 1 Then
MsgBox "No records found on " & ws.Name, vbCritical
Exit Sub
Else
Set rng = .Cells(ROW_START, COL_NAMES).Resize(n)
arNames = rng.Value2
'Write how many names were read into array
.Cells(1, 3) = "Number of names read into array is " & n
End If
' clear results
With rng.Offset(, 1).Resize(, 3)
.Clear
.Interior.Pattern = xlNone
End With
' run commands with WsSCript
Dim WShell As Object
Set WShell = CreateObject("WScript.Shell")
For i = 1 To UBound(arNames)
awsname = arNames(i, 1)
r = ROW_START + i - 1
' RUN COMMANDS
.Cells(r, COL_ERR) = RunTest(awsname, DIR_OUT)
'.Cells(r, COL_ERR) = RunCmd(WShell, awsname, DIR_OUT)
.Cells(r, COL_TS) = Format(Now, "yyyy-mm-dd HH:MM:SS") ' timestamp
Next
Set WShell = Nothing
'Write how many batch files were run into worksheet
.Cells(2, 3) = "Number of batch files run is " & UBound(arNames)
' read text files with FSO, parse with regex
Dim FSO As Object, ts As Object, regex As Object, txt As String
Set FSO = CreateObject("Scripting.FileSystemObject")
Set regex = CreateObject("VBScript.RegExp")
With regex
.Global = False
.MultiLine = True
.IgnoreCase = True
.Pattern = "with error code (\d+)"
End With
n = 0
' process text file
For i = 1 To UBound(arNames)
r = ROW_START + i - 1
awsname = arNames(i, 1)
txtfile = DIR_OUT & awsname & ".txt"
result = ""
' does file exist for this machine
If FSO.fileExists(txtfile) Then
' read file
n = n + 1
Set ts = FSO.openTextfile(txtfile)
txt = ts.readall
ts.Close
' extract error number from text
If regex.test(txt) Then
result = regex.Execute(txt)(0).submatches(0)
End If
' error codes
If result = "0" Then
colour = RGB(0, 255, 0) ' green
Else
colour = RGB(255, 255, 0) ' yellow
End If
Else
result = "No Text File"
colour = RGB(255, 0, 0) ' red
End If
' result
With .Cells(r, COL_RESULTS)
.Value2 = result
.Interior.Color = colour
End With
Next
.Cells(3, 3) = "Number of txt files found is " & n
.Columns.AutoFit
End With
MsgBox "Text files found for " & n, vbInformation, Format(Timer - t0, "0.0 secs")
End Sub
Function RunTest(awsname As String, folder As String) As String
Dim FSO, ts, errno: Set FSO = CreateObject("Scripting.FileSystemObject")
If Rnd() < 0.3 Then errno = 0 Else errno = Int(10000 * Rnd())
Set ts = FSO.createTextFile(folder & awsname & ".txt")
ts.write "This is with error code " & errno & "." & vbCrLf & vbCrLf
ts.Close
RunTest = "Test"
End Function
Function RunCmd(WShell, awsname As String, folder As String) As String
MsgBox "RunCmd DISABLED", vbCritical: End
'Const SCRIPT = "D:\min425.cmd"
'Dim cmd: cmd = SCRIPT & " " & awsname
'RunCmd = WShell.Run(cmd, vbNormal, True) ' waittocomplete
End Function

Skip rows with empty cells

I'm working on automating orders over WhatsApp with an Excel Sheet.
My programming experience is bare bones, so I used tutorials and other stack overflow threads to get to solutions. I have something that works, but these scripts send the full lists even if the item quantity cell is empty, which doesn't work for me.
From my understanding I need a If Else statement to do this, but I dont know where to place it.
The goal is to if a cell in the column is empty that row is skipped. How can I do that?
The below is the script that opens the browser and sends the messages.
Sub WebWhatsApp()
Dim pop As Range
Dim BOT As New WebDriver
Dim KS As New Keys
Dim count_row As Integer
count_row = ActiveSheet.Cells(Rows.Count, "A").End(xlUp).Row
Dim rng As Range
Set rng = Sheets("Interface").Range(Cells(12, 1), Cells(count_row, 5))
Dim myString As String
myString = Rang2String(rng)
BOT.Start "chrome", "https://web.whatsapp.com/"
BOT.Get "/"
MsgBox _
"Please scan the QR code." & _
"After you are logged in, please confirm this message box by clicking 'ok'", vbOKOnly, "WhatsApp Bot"
searchtext = Sheets("Interface").Range(Cells(5, 8), Cells(5, 8))
textmessage = myString
BOT.FindElementByXPath("//*[#id='side']/div[1]/div/label/div/div[2]").Click
BOT.Wait (500)
BOT.SendKeys (searchtext)
BOT.Wait (500)
BOT.SendKeys (KS.Enter)
BOT.Wait (500)
BOT.SendKeys (textmessage)
BOT.Wait (1000)
BOT.SendKeys (KS.Enter)
MsgBox "Done."
End Sub
And this is the script that turns a range of the Excel sheet into a string that the main script sends out as text messages.
Function Rang2String(rng As Range) As String
Dim strng As String
Dim myRow As Range
Dim KS As New Keys
With rng
For Each myRow In .Rows
strng = strng & Join(Application.Transpose(Application.Transpose(myRow.Value)), " | ") & vbNewLine
Next
End With
Rang2String = Left(strng, Len(strng) - 1)
End Function
I realize that the answer could be very obvious but I cant seem to see a solution.
Thanks in advance.
Option Explicit
Function Rang2String(rng As Range) As String
Const COL_QU = "D" ' quantity column
Dim e As String, myrow As Range
With Application
For Each myrow In rng.Rows
If Len(myrow.Cells(1, COL_QU)) > 0 Then
Rang2String = Rang2String & e & Join(.Transpose(.Transpose(myrow)), " | ")
e = vbNewLine
End If
Next
End With
End Function

Improving efficiency on background log routine

I created a log routine that creates a module in the desired file that records all changes for future auditing based on workbook events. I would like to com up with an alternative that I can activate at the start of a long process of routines applied to 100.000 rows, which mine doesn't seem to be able to support.
My log routine seems to work fine when activated in a blank worksheet, however it can't record all the changes made by my series of soubroutines. As it keeps track of each individual cell change in value and there are series of changes over the 100.000 rows, it crashes the application. I have been trying to think of a way to adapt it to be more efficient for my use, but so far I have been out of my depth.
Below is the code I import into the processed file to keep track of changes. If deemed necessary I can also post the routine that imports it.
public strOldAddress As String
Private Sub Worksheet_Change(ByVal Target As Range)
Dim rngSubTarget As Range
Dim lngBothCounter As Long
Dim Post() As String
'\ Parameters to register changes
Dim wsLog As Worksheet
Dim lngLogInputRow As Long
Set wsLog = ThisWorkbook.Sheets("Log")
'\ Detect changes in value
lngBothCounter = 1
ReDim Post(1 To Target.Cells.Count)
For Each rngSubTarget In Target.Cells
'\ Error Handler for changed values
If IsError(rngSubTarget.Value) Then
Post(lngBothCounter) = "ERROR"
Else
Post(lngBothCounter) = rngSubTarget.Value
End If
'\ Debug.Print for each value Ante and Post
'Debug.Print Post(lngBothCounter); " e " & Ante(lngBothCounter)
'\ Add changes values to log
If Ante(lngBothCounter) <> Post(lngBothCounter) Then
rngSubTarget.Interior.ColorIndex = 37
lngLogInputRow = wsLog.Range("A" & Rows.Count).End(xlUp).Row + 1
wsLog.Cells(lngLogInputRow, 1).Value = wsLog.Cells(lngLogInputRow, 1).Row - 1
wsLog.Cells(lngLogInputRow, 2).Value = Ante(lngBothCounter)
wsLog.Cells(lngLogInputRow, 3).Value = Post(lngBothCounter)
wsLog.Cells(lngLogInputRow, 4).Value = " " & rngSubTarget.Formula
wsLog.Hyperlinks.Add anchor:=wsLog.Cells(lngLogInputRow, 5), Address:="", _
SubAddress:="'" & ThisWorkbook.Sheets(1).Name & "'!" & rngSubTarget.Address, TextToDisplay:=rngSubTarget.Address
wsLog.Hyperlinks.Add anchor:=wsLog.Cells(lngLogInputRow, 6), Address:="", _
SubAddress:="'" & ThisWorkbook.Sheets(1).Name & "'!" & strOldAddress, TextToDisplay:=strOldAddress
wsLog.Cells(lngLogInputRow, 7).Value = Environ("username")
wsLog.Cells(lngLogInputRow, 8).Value = Now
End If
lngBothCounter = lngBothCounter + 1
Next rngSubTarget
End Sub
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Dim rngSubTarget As Range
Dim lngAnteCounter As Long
lngAnteCounter = 1
ReDim Ante(1 To Target.Cells.Count)
For Each rngSubTarget In Target.Cells
'\ Error Handling for values in selection
If IsError(rngSubTarget.Value) Then
Ante(lngAnteCounter) = "ERROR"
Else
Ante(lngAnteCounter) = rngSubTarget.Value
End If
lngAnteCounter = lngAnteCounter + 1
Next rngSubTarget
strOldAddress = Target.Address
End Sub
I expected it to keep track of all changes but when too many modifications are made through a macro it crashes the application (the log file is blank until I try to save the file, when it crashes).

Range constraint and variable management when passing information from macro to userform to worksheet

I've built a userform that allows modification of a macro-generated string before it becomes part of a new spreadsheet. As written, I have one worry about how resilient it will be.
The form has a single textbox called CourseDescription into which a string value strBundleDescription is dumped:
frmDescriptionReview.CourseDescription = strBundleDescription
frmDescriptionReview.CourseDescription.MultiLine = True
frmDescriptionReview.CourseDescription.WordWrap = True
frmDescriptionReview.Show
The user can then edit the text as needed and press OK to pass the text to the spreadsheet being created.
On clicking OK, the modified string is placed in Range("B7") of the spreadsheet:
Private Sub cmdOK_Click()
Dim strValue As String
strValue = CourseDescription.Value
If strValue <> "" Then
Range("B7").Value = strValue
End If
Unload Me
End Sub
This works so far in practice, but I've had unexplained focus issues before. I am concerned that the focus might in some (unknown) circumstance shift to another open worksheet and the text will be pasted where it does not belong.
My question: Am I right to want a more defined location, or will a simple range definition like the one above be adequate? And if a more defined location is advised, is there a way to pass information like the wkbSaba and shtCourse values without making public variables?
All potential solutions I found involved some form of public variable, but on principle (rightly or wrongly) I'm trying to avoid public variables when information will only be used in one function (as in this case).
Full Code, as requested: This is the the full macro code as it stands. The call for frmDescriptionReview is about 3/4 of the way down under the comment tag "'enter base information for Bundle Description".
I'm going to try the Property call as you suggest, which is something I did not know about, and had not seen when web searching for ways to pass data to a userform. So much to learn! It certainly looks like the variables could be passed that way.
Option Explicit
Sub TransferData()
'***************************************
' TO USE THIS MACRO:
' 1. Make sure that all information for the bundle is included
' on the 'km notification plan' and 'bundle details (kbar)' tabs
' of the Reporting_KMFramework.xlsx
' 2. Select the bundle name on the 'km notification plan' tab.
' 3. Start the macro and it should create the basis of the Saba
' form
' 4. Read through the entire form, especially the bundle
' description, to be sure it is complete and accurate.
'***************************************
'establish variables
Dim iRow As Integer
Dim sTxt As String
Dim sTxt2 As String
Dim sBundleName As String
Dim sNumber As String
Dim aSplit() As String
Dim aSplit2() As String
Dim aBundleSplit() As String
Dim aNumberSplit() As String
Dim wkbFramework As Workbook
Dim wkbSaba As Workbook
Dim shtPlan As Worksheet
Dim shtCourse As Worksheet
Dim vData As Variant
Dim vBundleName As Variant
Dim lLoop As Long
'set initial values for variables
'find current row number
iRow = ActiveCell.Row
'remember locations of current data
Set wkbFramework = ActiveWorkbook
Set shtPlan = ActiveSheet
'Set rngSelect = Range("B" & iRow)
'select bundle name
vBundleName = shtPlan.Range("B" & iRow).Value
vData = vBundleName
sBundleName = shtPlan.Range("B" & iRow).Value
'find and save course names for the bundle
Sheets(2).Select
sTxt = Find_Range(vBundleName, Columns("B"), xlValues).Offset(0, 1).Value 'course names from Detail tab
sTxt2 = Find_Range(vBundleName, Columns("B"), xlValues).Offset(0, 2).Value 'course numbers from Detail tab
'open new Saba Form
Workbooks.Add Template:= _
"C:\Documents and Settings\rookek\Application Data\Microsoft\Templates\Bundle_SabaEntryForm_KM.xltm"
'remember locations of Saba form
Set wkbSaba = ActiveWorkbook
Set shtCourse = ActiveSheet
'move data into new Saba form
'paste bundle name
wkbSaba.Sheets(shtCourse.Name).Range("B5").Value = vData
'Transfer bundle number
vData = wkbFramework.Sheets(shtPlan.Name).Range("E" & iRow).Value
sNumber = vData
Dim aNumber() As String
aNumber = Split(sNumber, "-")
wkbSaba.Sheets(shtCourse.Name).Range("B6").Value = vData
'create names to use in the bundle description and (later) in naming the file
'Establish additional variables
Dim strDate As String
Dim strName1 As String
Dim strName2 As String
Dim strName3 As String
Dim strName4 As String
Dim strName5 As String
Dim aTechSplit() As String
Dim aCourse() As String
Dim iTech As Integer
'Dim iBundle As Integer
Dim iCourse As Integer
vData = wkbFramework.Sheets(shtPlan.Name).Range("L" & iRow).Value
aCourse = Split(sTxt, Chr(10))
iCourse = UBound(aCourse)
aTechSplit = Split(vData, " ")
iTech = UBound(aTechSplit)
aBundleSplit = Split(sBundleName, " ")
aNumberSplit = Split(sNumber, "-")
strName1 = aBundleSplit(0)
strName2 = aBundleSplit(1)
If UBound(aNumberSplit) > 1 Then
strName3 = aNumberSplit(UBound(aNumberSplit) - 1) & aNumberSplit(UBound(aNumberSplit))
End If
strName3 = Right(strName3, Len(strName3) - 1)
strName4 = aTechSplit(0) & " "
strName5 = aCourse(0)
For lLoop = 1 To iTech - 1
strName4 = strName4 & aTechSplit(lLoop) & " "
Next lLoop
If iCourse > 1 Then
For lLoop = 1 To iCourse - 1
strName5 = strName5 & ", " & aCourse(lLoop)
Next lLoop
strName5 = strName5 & ", and " & aCourse(iCourse)
End If
If iCourse = 1 Then
strName5 = strName5 & ", and " & aCourse(iCourse)
End If
strName5 = Replace(strName5, " Technical Differences", "")
strName5 = Replace(strName5, " Overview", "")
strName5 = Replace(strName5, " Technical Presales for ATCs", "")
strName5 = Replace(strName5, " Technical Presales for STCs", "")
strName5 = Replace(strName5, " Technical Presales", "")
'enter base information for Bundle Description
Dim strBundleDescription As String
strBundleDescription = "This Knowledge Maintenance bundle covers recent technology changes that may affect " & strName4 & "environments. Topics covered by this bundle include the enhancements and features introduced with " & strName5 & "."
'wkbSaba.Sheets(shtCourse.Name).Range("B7").Value = strBundleDescription
frmDescriptionReview.CourseDescription = strBundleDescription
frmDescriptionReview.CourseDescription.MultiLine = True
frmDescriptionReview.CourseDescription.WordWrap = True
frmDescriptionReview.Show
'transfer tech and track
wkbSaba.Sheets(shtCourse.Name).Range("B8").Value = vData
'transfer product GA date
vData = wkbFramework.Sheets(shtPlan.Name).Range("G" & iRow).Value
wkbSaba.Sheets(shtCourse.Name).Range("B9").Value = vData
'transfer bundle notification date
vData = wkbFramework.Sheets(shtPlan.Name).Range("D" & iRow).Value
wkbSaba.Sheets(shtCourse.Name).Range("B10").Value = vData
'set audience type
If aNumber(UBound(aNumber)) = "SA" Then
wkbSaba.Sheets(shtCourse.Name).Range("B11").Value = "Internal, Partner, Customer"
Else
wkbSaba.Sheets(shtCourse.Name).Range("B11").Value = "Internal, Partner"
End If
'set Education Manager
frmEducationManagerEntry.EducationManagers.MultiLine = True
frmEducationManagerEntry.EducationManagers.WordWrap = True
frmEducationManagerEntry.Show
'set EPG
wkbSaba.Sheets(shtCourse.Name).Range("B13").Value = "N/A (KM course reuse)"
'set Test information to N/A
wkbSaba.Sheets(shtCourse.Name).Range("A22:B22").Value = "N/A"
'enter course names
aSplit = Split(sTxt, Chr(10)) 'if there is more than one course, this establishes a number and location for each
If UBound(aSplit) > 4 Then
'add rows equal to the difference between ubound and 5
wkbSaba.Sheets(shtCourse.Name).Range("A21", "B" & 21 + (UBound(aSplit) - 5)).Select
Selection.EntireRow.Insert
End If
For lLoop = 0 To UBound(aSplit)
wkbSaba.Sheets(shtCourse.Name).Range("B" & 17 + lLoop).Value = aSplit(lLoop)
Next lLoop
'enter course numbers
aSplit2 = Split(sTxt2, Chr(10)) 'if there is more than one course, this establishes a number and location for each
For lLoop = 0 To UBound(aSplit2)
wkbSaba.Sheets(shtCourse.Name).Range("A" & 17 + lLoop).Value = Trim(aSplit2(lLoop))
Next lLoop
'save and close Saba form
With wkbSaba.Sheets(shtCourse.Name)
Dim SaveAsDialog As FileDialog
strDate = Date
strDate = Replace(strDate, "/", ".")
Set SaveAsDialog = Application.FileDialog(msoFileDialogSaveAs)
With SaveAsDialog
.Title = "Choose a file location and file name for your new Saba form"
.AllowMultiSelect = False
.InitialFileName = strName1 & strName2 & "_SabaEntryForm_" & strName3 & ".xlsx"
'.InitialFileName = sSavelocation & "\" & strName3 & "\" & aBundleSplit(0) & aBundleSplit(1) & "_" & strName3 & "_SabaEntryForm" & ".xlsx"
.Show
.Execute
End With
wkbSaba.Sheets(shtCourse.Name).PrintOut
wkbSaba.Close
End With
' Return focus to Plan sheet
shtPlan.Activate
End Sub
Addition of Property code fails
I tried adding code based on the property link shared in the comments, but running the code results in a Compile error: Method or data member not found. The complete userform code looks like this:
Option Explicit
Private wkbLocation As Workbook
Private shtLocation As Worksheet
Private Sub cmdCancel_Click()
Unload Me
End
End Sub
Private Sub cmdOK_Click()
Dim strValue As String
strValue = CourseDescription.Value
If strValue <> "" Then
wkbLocation.Sheets(shtLocation).Range("B7").Value = strValue
End If
Unload Me
End Sub
Property Let MyProp(wkbSaba As Workbook, shtCourse As Worksheet)
wkbLocation = wkbSaba
shtLocation = shtCourse
End Property
And the call for the userform now looks like this:
'enter base information for Bundle Description
Dim strBundleDescription As String
strBundleDescription = "This Knowledge Maintenance bundle covers recent technology changes that may affect " & strName4 & "environments. Topics covered by this bundle include the enhancements and features introduced with " & strName5 & "."
'wkbSaba.Sheets(shtCourse.Name).Range("B7").Value = strBundleDescription
Dim frmDescriptionReview As UserForm3
Set frmDescriptionReview = New UserForm3
frmDescriptionReview.MyProp = "Pass to form"
frmDescriptionReview.CourseDescription = strBundleDescription
frmDescriptionReview.CourseDescription.MultiLine = True
frmDescriptionReview.CourseDescription.WordWrap = True
frmDescriptionReview.Show
When I run the code, I get a Compile error: Method or data member not found, highlighting .MyProp. Help says this error means I misspelled the object or member name, or specified a collection index that is out of range. I checked the spelling, and MyProp is exactly how I spelled it in both locations. I don't think I'm specifying a collection am I? None are explicitly defined. What am i doing wrong?
I am concerned that the focus might in some (unknown) circumstance
shift to another open worksheet and the text will be pasted where it
does not belong.
Not really sure what you are asking. But you can further define your range variable by using:
Workbooks("Book1.xlsm").Worksheets("Sheet1").Range("B7").Value = strValue
or
Workbooks(wkbSaba).Worksheets(shtCourse).Range("B7").Value = strValue
That will ensure it goes to the right workbook and worksheet. I'm not sure why you think you need public variables?
EDIT:
UserForm Code:
Private wsSheet As Worksheet
Property Let SetWorksheet(wsSheetPass As Worksheet)
Set wsSheet = wsSheetPass
End Property
Private Sub cmdOK_Click()
Dim strValue As String
strValue = CourseDescription.Value
If strValue <> "" Then
wsSheet.Range("B7").Value = strValue
End If
Unload Me
End Sub
Calling Module:
Dim wsSheetToPass As Worksheet
Set wsSheetToPass = Workbooks(wkbSaba).Worksheets(shtCourse)
frmDescriptionReview.SetWorksheet = wsSheetToPass
As Reafidy states, creating a Property for the Userform and passing information to it would clearly be the right answer for passing variables to and from a userform.
Ideally what I want is to have the form very losely coupled with the module, and not touch the spreadsheet at all (so when appropriate I can pass information to the form from other modules, get the information returned, and place it where appropriate for the current module (which could be on an entirely different spreadsheet or in a completely different cell).
I found additional information on passing data with properties on the PeltierTech web site (http://peltiertech.com/Excel/PropertyProcedures.html) that helped me understand what Reafidy was doing so I couls start loosening the coupling between my code and my forms even more (which was my original intent for this question.
Adding the Get property allows the loose coupling I'm looking for, allowing me to both give and receive information without having to pass the spreadsheet data at all. So my call in the module now looks like this:
'review and revise Description Text
Dim DescriptionReview As New frmDescriptionReview
With DescriptionReview
.Description = strBundleDescription
.Show
strBundleDescription = .Description
End With
Unload DescriptionReview
'transfer description text
wkbSaba.Sheets(shtCourse.Name).Range("B7").Value = strBundleDescription
and the code for the UserForm itself becomes much simpler, like this:
Option Explicit
Property Let Description(ByVal TextBeingPassed As String)
Me.CourseDescription.Value = TextBeingPassed
End Property
Property Get Description() As String
Description = Me.CourseDescription.Value
End Property
Private Sub cmdOK_Click()
Me.Hide
End Sub
Private Sub cmdCancel_Click()
Unload Me
End
End Sub

Resources