Excel VBA make a script asynchronous - vba

I have a script that can ping a list of computers and change their background color depending after the result it gets.
My problem is, that it blocks the entire excel file while it runs.
So my question is, how can I make it to run async?
Here is the code:
'ping
Function sPing(sHost) As String
Dim oPing As Object, oRetStatus As Object
Set oPing = GetObject("winmgmts:{impersonationLevel=impersonate}").ExecQuery _
("select * from Win32_PingStatus where address = '" & sHost & "'")
For Each oRetStatus In oPing
If IsNull(oRetStatus.StatusCode) Or oRetStatus.StatusCode <> 0 Then
sPing = "timeout" 'oRetStatus.StatusCode <- error code
Else
sPing = sPing & vbTab & oRetStatus.ResponseTime & Chr(10)
End If
Next
End Function
Sub pingall_Click()
Dim c As Range
Dim p As String
Application.ScreenUpdating = True
For Each c In ActiveSheet.Range("A1:N50")
If Left(c, 7) = "172.21." Then
p = sPing(c)
If p = "timeout" Then
c.Interior.ColorIndex = "3"
ElseIf p < 16 And p > -1 Then
c.Interior.ColorIndex = "4"
ElseIf p > 15 And p < 51 Then
c.Interior.ColorIndex = "6"
ElseIf p > 50 And p < 4000 Then
c.Interior.ColorIndex = "45"
Else
c.Interior.ColorIndex = "15"
End If
End If
Next c
Application.ScreenUpdating = False

You can't do too much about this unfortunately since VBA runs in a single thread.
You can however introduce a degree of responsiveness by putting
VBA.DoEvents()
in various places in your code, ideally in the tight loops. In your case, put them just after the lines containing For. This pauses the VBA and flushes the event queue which will have the effect of making Excel responsive.
(Toggling the screen updating is a bad idea since you might leave things in a bad state if the function terminates unexpectedly. I'd remove the lines that do that if I were you.)

Excel can calculate "asynchronously". Call sPing as a function.
I'm not sure why your range is A1:N50. I assume one of the columns is the IP address, which I will assume as A. So your formula in column M will look like =sPing(A1).
As for the color coding, you can use conditional formatting.

While strictly speaking you cannot make Excel to behave as you need, there is a trick to work around it. The solution is to create another Excel instance. Then you can run the macro in one of the instances and work independently in the other one.
You can open another Excel instance from the Run prompt (press Windows + R) and then type Excel /x, then Enter

Related

EPPlus fails to set formulas. Instead, it corrupts the formula XML

When I try to set cell formulas in an existing Excel file via EPPlus, the excel document is corrupted. Excel throws "We found a problem with some content in 'Excel.xlsx'. Do you want us to try to recover as much as we can? If you trust the source of this workbook, click Yes." dialog box, then says, "Removed Records: Formula from /xl/worksheets/sheet1.xml part"
If I comment out the formula set operations, the error goes away, but it fills in with formulas I didn't ask for anywhere in my code.
I have an excel file with several sheets. On one sheet, I want to set a value in column L and set formulas in columns I, J, and M. If I do this manually within Excel, everything works without error. But when I try to automate it, I Get the error messages and lose the formulas.
intended formulas:
Formula for column I: =IFNA(VLOOKUP([#[SQL Server]],SqlVersions!$C:$R,12, TRUE),"--")
Formula for column J: =IFNA(VLOOKUP([#[SQL Server]],SqlVersions!$C:$S,17,TRUE),"--")
Formula for column M: =IFNA(VLOOKUP([#[SQL Server]],SqlVersions!$C:$R,8,TRUE), "--")
Dim Hdr As String = ""
dim serverData as New List (of string) 'a list of data like A1||ServerName
' SNIP <get list data from database.> /SNIP
Dim fInfo As New FileInfo(excelFile)
Using ePack As New ExcelPackage(fInfo)
Dim mySheet As ExcelWorksheet = Nothing
'find the sheet we need.
For Each sheet As ExcelWorksheet In ePack.Workbook.Worksheets
If sheet.Name = ExcelServers Then
mySheet = sheet
Exit For
End If
Next
If IsNothing(mySheet) Then Throw New Exception("Server sheet not found.")
For Each serverRow in ServerData
If IsNothing(serverRow) OrElse InStr(serverRow, "||") = 0 Then Continue For 'skip "blank" rows
Dim Cell() As String = Split(serverRow, "||")
Dim CellAddress As String = Cell(0) 'A1..A50
Dim CellValue As String = Trim(Cell(1)) 'ServerName or table header
Dim CellAddressCol As String = Left(CellAddress, 1) ' Will always be A
Dim CellAddressRow As Integer = CellAddress.Substring(1) 'number, 1-50
If CellValue = "Oracle Server" Then
Hdr = "Ora" 'we've found a list of Oracle servers
Continue For 'skip ahead to the next value
ElseIf CellValue = "SQL Server" Then
Hdr = "Sql" 'we're done with Oracle, moving on to SQL Server servers
Continue For 'skip ahead to the next value
ElseIf CellValue = "Non-DB Servers" Then
Exit For 'we're done with all of our work.
End If
If Hdr = "Ora" Then
If Len(CellValue) < 2 Then
mySheet.Cells("L" & CellAddressRow).Value = ""
Else
mySheet.Cells("L" & CellAddressRow).Value = "P"
End If
ElseIf Hdr = "Sql" Then
If Len(CellValue) < 2 Then
mySheet.Cells("I" & CellAddressRow).Value = ""
mySheet.Cells("J" & CellAddressRow).Value = ""
mySheet.Cells("L" & CellAddressRow).Value = ""
mySheet.Cells("M" & CellAddressRow).Value = ""
ElseIf CellValue = "Cluster1" Or CellValue = "Cluster2" Then
mySheet.Cells("I" & CellAddressRow).Value = ""
mySheet.Cells("J" & CellAddressRow).Value = ""
mySheet.Cells("L" & CellAddressRow).Value = "C"
mySheet.Cells("M" & CellAddressRow).Value = ""
Else 'data row.
mySheet.Cells("I" & CellAddressRow).Formula = "IFNA(VLOOKUP([#[SQL Server]],SqlVersions!$C:$R,12, TRUE),""--"")"
mySheet.Cells("J" & CellAddressRow).Formula = "IFNA(VLOOKUP([#[SQL Server]],SqlVersions!$C:$S,17,TRUE),""--"")"
mySheet.Cells("L" & CellAddressRow).Value = "V"
mySheet.Cells("M" & CellAddressRow).Formula = "ifNA(VLOOKUP([#[SQL Server]],SqlVersions!$C:$R,8,FALSE),""--"")"
End If ' /empty row? Cluster row? other server row?
End If ' /Oracle or SQL?
Next
ePack.Save()
End Using
I expect to get a series of rows where the rows after "Oracle" get a "P" in column L and the rows after "SQL Server" have lookup formulas in columns I, J, and M, with a "V" in column L.
If I leave the .Formula = code in place, I get the errors. If I comment out the .Formula lines, I instead get the formula "=70+65" for Oracle rows and "=159+799" for SQL Server rows.
The end result should look something like this:
(Note that the Oracle rows and two header rows are just text and aren't modified by this code.)
Oracle Server,,,,,,,,Version,Patch,,P V or C, End of Life
Oracle1,,,,,,,,12.2.0.1,27937914,,P,
Oracle,,,,,,,,12.2.0.1,27937914,,P,
,,,,,,,,
Sql Server,,,,,,,,Version,Patch,,P V or C,End of Life
Cluster1,,,,,,,,,,,C,7/14/2026
Cluster2,,,,,,,,,,C,
Sql1,,,,,,,2016 Ent 13.0.5337,SP2 CU7 Up,,V,10/12/2027
Sql2,,,,,,,2017 Ent 14.0.3223,CU16,,V,7/14/2026
[...]
sql32,,,,,,,2016 Ent 13.0.5426,SP2 CU8,,V,7/14/2016
,,,,,,,,
Non-DB Servers,,,,,,,,
But what I'm getting, after I accept the error message request to repair is:
Oracle Server,,,,,,,,Version,Patch,,P V or C, End of Life
Oracle1,,,,,,,,12.2.0.1,27937914,,135,
Oracle,,,,,,,,12.2.0.1,27937914,,135,
,,,,,,,,,,,135
Sql Server,,,,,,,,Version,Patch,,P V or C,End of Life
Cluster1,,,,,,,,,,958,#N/A
Cluster2,,,,,,,,,,958,#N/A
Sql1,,,,,,,,,,958,10/12/2027
Sql2,,,,,,,,,,958,7/14/2026
[...]
sql32,,,,,,,,,,958,7/14/2016
,,,,,,,,,,958,#N/A
Non-DB Servers,,,,,,,,
I have no idea where those formulas are coming from at all, as they are nowhere in my code, ever.
Edit
Here is the Excel file (scrubbed of actual server names) as it should be.
And here is the Excel file (also scrubbed) as this code leaves it.
When I open and save() the sample you provided, without doing any edit whatsoever, it always shows me the problem with some content error (tried on Windows 10 with Office 365 and .NET 4.7.2). Because of this I cannot be certain of the cause in your specific situation.
However, I notice EPPlus has a problem when 'translating' formulae when using a Table within Excel. As an example a simple vlookup:
=VLOOKUP(A2,Data!A:B,2,FALSE)
When the above formula is used in a Table, it is changed after being saved with EPPlus and is now erroneous (showing #NAME?) because of the A:B:B:
=VLOOKUP(A2,Data!A:B:B,2,FALSE)
Finding out if this is a bug in EPPlus will require some extensive debugging, with the ExcelCellBase.Translate method being a good start. Also, someone else might already have found it (EPPlus has an relatively large list of open issues.
Sorry if this does not help. I think what I showed is a bug in EPPlus, but I do now know if it it the cause for your problem.

Extracting hyperlink from a range and writing them on another range

I'm new to this site, but I have already found some nice advice on how to solve problems in VBA. Now I'm here to ask help on a sub that gives me problems with Hyperlinks.
In particular, my problem is similar to the one described in this topic:
Excel VBA Get hyperlink address of specific cell
I have a worksheet full of hyperlink, but I need to extract only the addresses present in the "H" column, starting from "H6" and writing them into the "N" column, starting from "N6".
I put down this code:
Sub EstraiIndirizzoPut()
Dim IndirizzoInternet As Hyperlink
Dim ISINs As String
i = 6
For Each IndirizzoInternet In Sheets("XXX").Range("H" & i).Hyperlinks
IndirizzoInternet.Range.Offset(0, 6).Value = IndirizzoInternet.Address
ISINs = Mid(IndirizzoInternet.Address, 78, 12)
Range("N" & i).Value = ISINs
i = i + 1
Next
End Sub
It works fine only for the first "H6" cell, but at the "Next" point, when it should read the "H7" cell, it goes instead to "End Sub", terminating the routine, altough the "H7" cell, as well many others down the column, are filled with hyperlinks (it gives me "Nothing" value).
Could you please suggest me where I get this wrong? Many thanks.
Your loop isnt set up correctly. Try it like this instead:
For i = 6 to 100
Set IndirizzoInternet = Sheets("XXX").Range("H" & i).Hyperlinks
IndirizzoInternet.Range.Offset(0, 6).Value = IndirizzoInternet.Address
ISINs = Mid(IndirizzoInternet.Address, 78, 12)
Range("N" & i).Value = ISINs
Next
How do you know when to stop the loop? Is it a preset number of rows? If it not, you will want to have something determine the last row to process and replace the 100 with that variable.

.find() triggers run-time error 91 even though all variables are set VBA possibly due to bad references

I am writing code to create a template. This code populates a tab named "fullDistribution" from user-input on different tabs in the same wb. I have a working section of code that I wrote in a separate module (for testing) away from my master module. The code runs properly and executes completely when it is separate. When I pasted this section of code into my master module and ran it, I began receiving "Run-time error 91: object variable or with block variable not set" at the start of the newly-pasted code. I am not using any with blocks, and all of my variables are set. I made no changes in my code when I transferred it to my master module, and I carried over the new variables I created.
This is the selection of code that I wrote in a separate module:
Worksheets("bls2016").Activate
tcount = WorksheetFunction.CountA(Worksheets("detailedEntity").Range("D2:D" & Cells(Rows.Count, "D").End(xlUp).Row))
acount = WorksheetFunction.CountA(Worksheets("detailedEntity").Range("K2:K7"))
Application.ScreenUpdating = False
Dim h As Integer
Dim f As Integer
Dim blstate As Range
Dim bl As Range
Dim state As Range
Dim deat As Range
Dim agje As Range
Dim e As Integer
Dim r As Integer
Dim ii As Integer
Set blstate = Worksheets("bls2016").Range("D2:D" & Cells(Rows.Count, "D").End(xlUp).Row)
Set state = Worksheets("detailedEntity").Range("Q1")
Set deat = Worksheets("detailedEntity").Range("D2:D" & Cells(Rows.Count, "D").End(xlUp).Row)
Set agje = Worksheets("detailedEntity").Range("L2:M" & Cells(Rows.Count, "M").End(xlUp).Row)
h = Activecolumn
f = Activerow
r = 2
x = 120
For e = 1 To (acount * acount)
blstate.Find(state).Select
For ii = 1 To x
'ccnt = acst.Offset(0, 1)
ccgv = ActiveCell.Offset(0, 2)
acem = ActiveCell.Offset(0, 5)
Do While True
vl1 = Application.IfNa(Application.VLookup(Worksheets("fullDistribution").Cells(r, 2), deat, 1, False), 0)
If vl1 = 0 Then
Worksheets("fullDistribution").Cells(r, 4) = 0
Else:
vl2 = Application.IfNa(Application.VLookup(Worksheets("fullDistribution").Cells(r, 1), agje, 2, False), 0)
If ActiveCell.Offset(0, 1).Value = "Unknown Or Undefined" Then
Exit Do
Else:
If vl2 = ccgv Then
Worksheets("fullDistribution").Cells(r, 4) = acem
ElseIf vl2 <> ccgv Then
Worksheets("fullDistribution").Cells(r, 4) = ActiveCell.Offset(x + 1, 5)
Else:
End If
End If
End If
Exit Do
Loop
ActiveCell.Offset(f + 1, h).Select
r = r + 1
Next ii
Next e
The error triggers at the line "blstate.find(state).select" which tells excel to look in a dynamic range that contains the names of states and select the first instance of the state to use as the Activecell. Again, this works when it's run outside of the main module.
I believe this has something to do with a reference area. When this runs alone and finishes, I have to have a specific worksheet activated for it to run properly. If my excel workbook is open to a different tab, it will not run. My main module too only executes properly if it is run on a specific worksheet/tab.
If need be, I can edit my post and provide my whole master code.
It may be a problem of not fully referencing sheets, eg amend your blstate line to
with Worksheets("bls2016")
Set blstate = .Range("D2:D" & .Cells(.Rows.Count, "D").End(xlUp).Row)
end with
Then it might find the value and not error. You should look up how to use the Find method as your way is destined to cause you headaches.
blstate.Find(state).Select
Your code assumes that .Find finds what it's looking for. When Find doesn't find what it's looking for, the function returns Nothing, which is essentially a null object reference - and you can't make member calls on Nothing without getting run-time error 91.
Split it up:
Dim result As Range
Set result = blstate.Find(state)
If Not result Is Nothing Then
result.Select 'questionable anyway, but that's another issue
Else
MsgBox "Value '" & state & "' was not found in " & blstate.Address(External:=True) & "."
Exit Sub
End If
As for why it's not finding what you're looking for, Tim Williams already answered that:
Find recalls all settings used in the last call (even if you use the GUI to perform the Find), so make sure you specify the settings you want when you call it via VBA. If you don't do that, it may not work as you expect.... – Tim Williams 42 mins ago
My issue was very much related to incorrect referencing, however, I was able to resolve this issue by keeping the specific piece of code I was testing in a separate sub, and calling it from my main code, 'full distribution'.
Call test
'test' is the name of the sub with the tested code. This is a temporary fix to the solution, and if anyone struggles with referencing, try this.

xlErrorChecks Enumeration Office 365/Excel 2016

In my current version of Excel, the enumeration of xlNumberAsText seems inconsistent with both the documentation XlErrorChecks Enumeration (Excel) and prior usage recommended on SO. It seems that particular error is Item 4; whereas previous posts about clearing this error using VBA have used a constant of 3.
Is this a bug in Excel 2016? Or am I doing something incorrectly.
Here is code demonstrating the issue. And if you examine the worksheet after running the code, it is apparent that the error is flagged, in Excel, as NumberAsText and not as Inconsistent Formula.
Option Explicit
Sub foo()
Dim I As Long
Dim B As Boolean
Dim S As String
Dim R As Range
'Save current state
B = Application.ErrorCheckingOptions.NumberAsText
'Enable
Application.ErrorCheckingOptions.NumberAsText = True
Set R = Cells(1, 1)
With R
.Clear
.NumberFormat = "#"
.Value = "1"
End With
For I = 1 To 10
S = S & vbLf & I & vbTab & R.Errors(I).Value
Next I
S = Mid(S, 2)
'Restore original state
Application.ErrorCheckingOptions.NumberAsText = B
MsgBox S
End Sub
And also, in trying to clear the error box, I must use
R.Errors(4).Ignore = True
Neither Errors(3), nor Errors(xlNumberAsText) will have any affect on the error box.
I've noted other inconsistencies with the enum and the documentation:
Inconsistent Formula: 5
Wrong Data Type: 2
I'm not going to bother to check the rest. But I am going to try to report it to MS. Thanks to those who confirmed this problem.
Followup: I reported this to MS via the Feedback option within Excel. And after this morning's update, the problem seems to have been corrected.

How should I Notify the user during a long process?

I have some long processes that require notifications to the user at successive stages, so that he doesn't get to believe Excel has crashed down.
How could I display asynchronous messages to the user in Excel, using VBA ?
You can use the status bar in Excel to do this:
Application.StatusBar = "status message"
Here is an example on how to implement this: http://www.vbaexpress.com/kb/getarticle.php?kb_id=87
Below is the code from the site (added line break to make is easier to read):
Sub StatusBar()
Dim x As Integer
Dim MyTimer As Double
'Change this loop as needed.
For x = 1 To 250
'Dummy Loop here just to waste time.
'Replace this loop with your actual code.
MyTimer = Timer
Do
Loop While Timer - MyTimer < 0.03
Application.StatusBar = _
"Progress: " & x & " of 250: " & Format(x / 250, "Percent")
DoEvents
Next x
Application.StatusBar = False
End Sub
UPDATE:
I do want to add that updating the status bar will result in a sizable hit in performance (quite a bit actually), so you should only update it in approriate intervals. Here's an example of what I mean (I use MOD here to ensure we only increment each 1000):
Sub test()
Dim i As Long
Dim temp As String
For i = 1 To 1000000
temp = "testing 123, testing 123"
If i Mod 1000 = 0 Then
Application.StatusBar = "Processing " & i & "/1,000,000..."
End If
Next
Application.StatusBar = "Ready"
End Sub
Also note that you want to reset the text to "Ready" otherwise it'll be left as if it were in the loop.
I've stuck with Walkenbach's progress form for my addins
The following article has a number of ways of doing this: http://oreilly.com/pub/h/2607
I think the best bet for you would be to show a progress form. This can include a progress bar and text updates to reassure the user.
Something I once did was to create an extra tab called "Running".
After each time consuming loop, I add the following code with updated text information.
Although the text sometimes changes too fast, the changing color bar shows the user that the script is still running. You have to define AlertColor first with a value of 6.
Sheets("Running").Select 'Show the running page
Range("B18").Value = "Importing ABC......"
Cells(18, 2).Interior.ColorIndex = AlertColour
AlertColour = AlertColour + 1
If AlertColour > 8 Then AlertColour = 6
Application.ScreenUpdating = True
Application.ScreenUpdating = False
I don't know how far you want to go with your solution, but you can utilise RTD function. That way you could put a status message directly in the worksheet. But it would require development of a COM Automation server, which is not complicated (can be written in .NET or VB6 or C++ or Delphi), but causes problems once in production (deployment, support, code control etc.)