I want to know if it is possible to get the IP of machine, logged on user, hostname from event id 4624 using vbscript?
I want a vbscript which takes out this information:
It's possible. You need to query events with the ID 4624 from the eventlog and then parse name, IP address and port out of the message string, e.g. with a regular expression:
Set wmi = GetObject("winmgmts://./root/cimv2")
Set re = New RegExp
re.Pattern = "Network Information:\s+" & _
"Workstation Name:\s*(.*?)\s+" & _
"Source Network Address:\s*(.*?)\s+" & _
"Source Port:\s*(\d+)"
qry = "SELECT * FROM Win32_NTLogEvent WHERE EventCode=4624"
For Each evt In wmi.ExecQuery(qry)
For Each m In re.Execute(evt.Message)
hostname = m.SubMatches(0)
address = m.SubMatches(1)
port = m.SubMatches(2)
Next
WScript.Echo hostname & " [" & address & ":" & port & "]"
Next
Basically it sounds like you're looking for this article. In it, the author outlines a very thorough approach but the key bit is:
Function ProcessScript
Dim hostName, logName, startDateTime, endDateTime
Dim events, eventNumbers, i
hostName = wshNetwork.ComputerName
logName = "Security"
eventNumbers = Array("672") ' This is a comma-delimited list of events. You would include 4212 here
startDateTime = DateAdd("n", -120, Now)
'-------------------------------------------------------------------------------------------------------------------------
'Query the event log for the eventID's within the specified event log name and date range.
'-------------------------------------------------------------------------------------------------------------------------
If Not QueryEventLog(events, hostName, logName, eventNumbers, startDateTime) Then
Exit Function
End If
End Function
This function calls into QueryEventLog, which does the heavy lifting:
Function QueryEventLog(results, hostName, logName, eventNumbers, startDateTime)
Dim wmiDateTime, wmi, query, eventItems, eventItem
Dim timeWritten, eventDate, eventTime, description
Dim eventsDict, eventInfo, errorCount, i
QueryEventLog = False
errorCount = 0
If Not IsArray(eventNumbers) Then
eventNumbers = Array(eventNumbers)
End If
'-------------------------------------------------------------------------------------------------------------------------
'Construct part of the WMI Query to account for searching multiple eventID's
'-------------------------------------------------------------------------------------------------------------------------
query = "Select * from Win32_NTLogEvent Where Logfile = " & SQ(logName) & " And (EventCode = "
For i = 0 To UBound(eventNumbers)
query = query & SQ(eventNumbers(i)) & " Or EventCode = "
Next
On Error Resume Next
Set eventsDict = NewDictionary
If Err.Number <> 0 Then
LogError "Creating Dictionary Object"
Exit Function
End If
Set wmi = GetObject("winmgmts:{impersonationLevel=impersonate,(Security)}!\\" & hostName & "\root\cimv2")
If Err.Number <> 0 Then
LogError "Creating WMI Object to connect to " & DQ(hostName)
Exit Function
End If
'----------------------------------------------------------------------------------------------------------------------
'Create the "SWbemDateTime" Object for converting WMI Date formats. Supported in Windows Server 2003 & Windows XP.
'----------------------------------------------------------------------------------------------------------------------
Set wmiDateTime = CreateObject("WbemScripting.SWbemDateTime")
If Err.Number <> 0 Then
LogError "Creating " & DQ("WbemScripting.SWbemDateTime") & " object"
Exit Function
End If
'----------------------------------------------------------------------------------------------------------------------
'Build the WQL query and execute it.
'----------------------------------------------------------------------------------------------------------------------
wmiDateTime.SetVarDate startDateTime, True
query = Left(query, InStrRev(query, "'")) & ") And (TimeWritten >= " & SQ(wmiDateTime.Value) & ")"
Set eventItems = wmi.ExecQuery(query)
If Err.Number <> 0 Then
LogError "Executing WMI Query " & DQ(query)
Exit Function
End If
'----------------------------------------------------------------------------------------------------------------------
'Convert the property values of Each event found to a comma seperated string and add it to the dictionary.
'----------------------------------------------------------------------------------------------------------------------
For Each eventItem In eventItems
Do
timeWritten = ""
eventDate = ""
eventTime = ""
eventInfo = ""
timeWritten = ConvertWMIDateTime(eventItem.TimeWritten)
eventDate = FormatDateTime(timeWritten, vbShortDate)
eventTime = FormatDateTime(timeWritten, vbLongTime)
eventInfo = eventDate & ","
eventInfo = eventInfo & eventTime & ","
eventInfo = eventInfo & eventItem.SourceName & ","
eventInfo = eventInfo & eventItem.Type & ","
eventInfo = eventInfo & eventItem.Category & ","
eventInfo = eventInfo & eventItem.EventCode & ","
eventInfo = eventInfo & eventItem.User & ","
eventInfo = eventInfo & eventItem.ComputerName & ","
description = eventItem.Message
'------------------------------------------------------------------------------------------------------------------------
'Ensure the event description is not blank.
'------------------------------------------------------------------------------------------------------------------------
If IsNull(description) Then
description = "The event description cannot be found."
End If
description = Replace(description, vbCrLf, " ")
eventInfo = eventInfo & description
'------------------------------------------------------------------------------------------------------------------------
'Check if any errors occurred enumerating the event Information
'------------------------------------------------------------------------------------------------------------------------
If Err.Number <> 0 Then
LogError "Enumerating Event Properties from the " & DQ(logName) & " event log on " & DQ(hostName)
errorCount = errorCount + 1
Err.Clear
Exit Do
End If
'------------------------------------------------------------------------------------------------------------------------
'Remove all Tabs and spaces.
'------------------------------------------------------------------------------------------------------------------------
eventInfo = Trim(Replace(eventInfo, vbTab, " "))
Do While InStr(1, eventInfo, " ", vbTextCompare) <> 0
eventInfo = Replace(eventInfo, " ", " ")
Loop
'------------------------------------------------------------------------------------------------------------------------
'Add the Event Information to the Dictionary object if it doesn't exist.
'------------------------------------------------------------------------------------------------------------------------
If Not eventsDict.Exists(eventInfo) Then
eventsDict(eventsDict.Count) = eventInfo
End If
Loop Until True
Next
On Error Goto 0
If errorCount <> 0 Then
Exit Function
End If
results = eventsDict.Items
QueryEventLog = True
End Function
The rest is detailed in that article, but basically just concerns itself with writing the results to a file and adding some nice user interactions around the execution.
Related
I have an excel macro enabled workbook which offers the user the option of entering some parameters to use in the query as filter (WHERE) clauses. This in turn is supplied to the queries. I have about 3 queries which do not use the filters and 4 OR 5 depending on which filters are chosen that run using filters. The query complexity varies.
The queries are run against a Redshift Cluster. (All of the data is confidential and the RS is internal connection only, so I can't give the entire query or anything, just examples)
The 3 small queries are 1-2 lines.
3 or 4 of the remaining 5 are about 40 lines
5th is about 100.
When run directly on the cluster with no filters: returns ~42400 rows and 23 Columns
3 small queries run and load to the excel file in less than 3 seconds or so each
Medium query 1: On Cluster - ~1 Seconds
Medium Query 2: On Cluster ~5 Seconds
Medium Query 3: On Cluster - ~9 Seconds
Large Query 1: On Cluster - ~24 seconds
Now here in lies the issue, when I run these queries in vba using the following for each query to update a listboject (example code) it takes 980.59 (~16.4 Minutes) Seconds
CS = "ODBC;Driver={Amazon Redshift (x64)};SERVER={RS1.us-east-1.redshift.amazonaws.com};PORT=8192;DATABASE=db1;UID=user;PASSWORD=fakepasswrod;sslmode=require"
With Sheet2.ListObjects.Add(SourceType:=0, Source:=CS, Destination:=Sheet2.Range("$A$1")).QueryTable
.CommandText = Sql
.RefreshStyle = xlInsertDeleteCells
.AdjustColumnWidth = True
.ListObject.DisplayName = "Name_of_LO_1"
.Refresh BackgroundQuery:=False
End With
In addiiton, I have to give the users the ability to do Wildcards, Comma Separated Lists, and single entries to filters. That part doesn't take long to build from the cell values.
I have to build the filters with large if statements similar to the one as follows
'Filter Fields
C_List = UCase(ThisWorkbook.Sheets(Sheet1.Name).Range("D1").Value)
S_List = UCase(ThisWorkbook.Sheets(Sheet1.Name).Range("D2").Value)
F_List = UCase(ThisWorkbook.Sheets(Sheet1.Name).Range("D3").Value)
s_year = ThisWorkbook.Sheets(Sheet1.Name).Range("D4").Value
Scen = "'" & ThisWorkbook.Sheets(Sheet1.Name).Range("D5").Value & "'"
prior_s_year_1 = "'" & ThisWorkbook.Sheets(Sheet1.Name).Range("D6").Value & "'"
prior_Scen_1 = "'" & ThisWorkbook.Sheets(Sheet1.Name).Range("D7").Value & "'"
prior_s_year_2 = "'" & ThisWorkbook.Sheets(Sheet1.Name).Range("D8").Value & "'"
prior_Scen_2 = "'" & ThisWorkbook.Sheets(Sheet1.Name).Range("D9").Value & "'"
cat = UCase(ThisWorkbook.Sheets(Sheet1.Name).Range("D10").Value)
subcat = UCase(ThisWorkbook.Sheets(Sheet1.Name).Range("D11").Value)
If Site_List = "" And Cluster_List = "" And FBN_List = "" Then
response = MsgBox("You have chosen no Site, Cluster or FBN filters, this will pull all data and may take some time" & vbNewLine & "Do you wish to continue?", vbYesNo)
If response = vbNo Then
Call MsgBox("Exiting data retrieval, please enter Site, Cluster or FBN filters and restart", vbOKOnly)
Call DeleteConnections
Exit Sub
End If
ElseIf C_List = "ALL" Then
UserDefinedFilters = " bd.reg IN ( SELECT DISTINCT c FROM att_1 ) "
ElseIf S_List <> "" And C_List <> "" And F_List <> "" Then
S_List = Replace(S_List, ", ", ",")
C_List = Replace(C_List, ", ", ",")
F_List = Replace(F_List, ", ", ",")
UserDefinedFilters = UserDefinedFilters & " UPPER(s) in ('" & Replace(S_List, ",", "','") & "')" & _
vbNewLine & " AND UPPER(reg) in ('" & Replace(C_List, ",", "','") & "')" & _
vbNewLine & " AND UPPER(f) in ('" & Replace(F_List, ",", "','") & "')"
ElseIf S_List <> "" And C_List <> "" And F_List = "" Then
S_List = Replace(S_List, ", ", ",")
Cluster_List = Replace(C_List, ", ", ",")
UserDefinedFilters = UserDefinedFilters & " UPPER(s) in ('" & Replace(S_List, ",", "','") & "')" & _
vbNewLine & " AND UPPER(reg) in ('" & Replace(C_List, ",", "','") & "')"
ElseIf S_List <> "" And C_List = "" And F_List = "" Then
S_List = Replace(S_List, ", ", ",")
UserDefinedFilters = UserDefinedFilters & " UPPER(s) in ('" & Replace(S_List, ",", "','") & "')"
ElseIf S_List = "" And C_List <> "" And F_List = "" Then
C_List = Replace(C_List, ", ", ",")
UserDefinedFilters = UserDefinedFilters & " UPPER(reg) in ('" & Replace(C_List, ",", "','") & "')"
ElseIf S_List = "" And C_List = "" And F_List <> "" Then
If InStr(1, F_List, ",") > 0 Then
F_List = Replace(F_List, ", ", ",")
UserDefinedFilters = UserDefinedFilters & " UPPER(bd.f) in ('" & Replace(UCase(F_List), ",", "','") & "')"
ElseIf InStr(1, F_List, "*") > 0 Then
UserDefinedFilters = UserDefinedFilters & " UPPER(bd.f) LIKE '%" & Replace(UCase(F_List), "*", "") & "%'"
ElseIf InStr(1, F_List, "ABC") > 0 Then
UserDefinedFilters = UserDefinedFilters & " UPPER(bd.f) LIKE '%" & UCase(Left(F_List, 12)) & "%'"
Else
UserDefinedFilters = UserDefinedFilters & " UPPER(bd.f) in ('" & UCase(F_List) & "')"
End If
ElseIf S_List = "" And C_List <> "" And F_List <> "" Then
If InStr(1, F_List, ",") > 0 Then
F_List = Replace(F_List, ", ", ",")
UserDefinedFilters = UserDefinedFilters & " UPPER(bd.f) in ('" & Replace(UCase(F_List), ",", "','") & "')"
ElseIf InStr(1, F_List, "*") > 0 Then
UserDefinedFilters = UserDefinedFilters & " UPPER(bd.f) LIKE '%" & Replace(UCase(F_List), "*", "") & "%'"
Else
UserDefinedFilters = UserDefinedFilters & " UPPER(bd.f) in ('" & UCase(F_List) & "')"
End If
End If
'Cat and SubCat Filters
If cat <> "" And subcat <> "" Then
cat = Replace(cat, ",", "','")
subcat = Replace(subcat, ",", "','")
BCSFilters = BCSFilters & " AND UPPER(sca.cat) IN ('" & cat & "')" & _
vbNewLine & "AND UPPER(sca.subcat) in ('" & subcat & "')"
ElseIf cat <> "" And subcat = "" Then
cat = Replace(cat, ",", "','")
BCSFilters = BCSFilters & " AND UPPER(sca.cat) IN ('" & cat & "')"
ElseIf cat = "" And subcat <> "" Then
subcat = Replace(subcat, ",", "','")
BCSFilters = BCSFilters & " AND UPPER(sca.subcat) IN ('" & subcat & "')"
End If
The above is only two sets, but it should give you the idea of what I am having to do for building the where clause.
I cannot find a way to get recordsets working using ADODB and I am not sure if that would be faster or not. I need to do this DSNless if at all possible because the file is used across a wide swath of users. Anything that anyone can think of that might help reduce this huge time in the queries?
EDIT:
Adding the code I attempted for records sets:
Dim conn As Object
Dim rs As Object
Set conn = CreateObject("ADODB.Connection")
Set rs = CreateObject("ADODB.Recordset")
CS = "Driver={Amazon Redshift (x64)};DATA SOURCE={RS1.us-east-1.redshift.amazonaws.com};PORT=8192;DATABASE=db1;UID=user;PWD=fakepasswrod;sslmode=require"
conn.Open CS
Set RegAtt = ThisWorkbook.Sheets(Sheet6.Name)
RegAtt.Cells.Clear
RegSql = "SELECT cl,reg,curr FROM schema.table1"
rs.Open RegSql
With RegAtt.ListObjects.Add(xlSrcQuery, rs, Destination:=RegAtt.Range("$A$1")).QueryTable
'.CommandText = RegSql
.RefreshStyle = xlInsertDeleteCells
.AdjustColumnWidth = True
.ListObject.DisplayName = "LO_2"
.Refresh BackgroundQuery:=False
End With
That connection string I get a driver not found error.
This CS = "Driver={Amazon Redshift (x64)};SERVER={RS1.us-east-1.redshift.amazonaws.com};PORT=8192;DATABASE=db1;ID=user;PASSWORD=fakepasswrod;sslmode=require"
I get 3709 - The connection cannot be used to perform this operation. It is either closed or invalid in this context.
This won't change the performance but you may find benefit in adopting a more object-orientated approach to building the queries. For example if you define a class module to hold the parameters and logic, then the build script becomes something like this ;
Sub BuildFilters()
Dim wb As Workbook, ws As Worksheet, response
Set wb = ThisWorkbook
Set ws = wb.Sheets(1)
Const msg1 = "You have chosen no Site, Cluster or FBN filters," & _
"this will pull all data and may take some time" & vbNewLine & _
"Do you wish to continue?"
Const msg2 = "Exiting data retrieval, please enter Site, Cluster or FBN filters and restart"
Dim Qb As New QueryBuilder
Qb.Init ws ' get parameters
If Qb.hasNone Then
response = MsgBox(msg1, vbYesNo)
If response = vbNo Then
Call MsgBox(msg2, vbOKOnly)
'Call DeleteConnections
End If
Else
' build SQL
Qb.BuildUDFilter
Qb.BuildBCSFilters
' dump to sheet to check result
ws.Range("D13") = Qb.UDFilter
ws.Range("D15") = Qb.BCSFilters
End If
End Sub
Class Module QueryBuilder
Option Explicit
Public BCSFilters As String
Public UDFilter As String
Dim C_List As String, hasC As Boolean
Dim S_List As String, hasS As Boolean
Dim F_List As String, hasF As Boolean
Dim s_year As String
Dim Scen As String
Dim prior_s_year_1 As String
Dim prior_Scen_1 As String
Dim prior_s_year_2 As String
Dim prior_Scen_2 As String
Dim cat As String, hasCat As Boolean
Dim subcat As String, hasSubcat As Boolean
Dim count As Integer, hasAny As Boolean
' Initialise Object from Sheet
Sub Init(ws As Worksheet)
With ws
C_List = .Cells(1, 4) ' D1
S_List = .Cells(2, 4)
F_List = .Cells(3, 4)
s_year = Cells(4, 4)
Scen = quoted(.Cells(5, 4))
prior_s_year_1 = quoted(.Cells(6, 4))
prior_Scen_1 = quoted(.Cells(7, 4))
prior_s_year_2 = quoted(.Cells(8, 4))
prior_Scen_2 = quoted(.Cells(9, 4))
cat = .Cells(10, 4)
subcat = .Cells(11, 4)
End With
hasC = CBool(Len(C_List))
hasS = CBool(Len(S_List))
hasF = CBool(Len(F_List))
hasCat = CBool(Len(cat))
hasSubcat = CBool(Len(subcat))
End Sub
Function hasNone() As Boolean
hasNone = Not (hasC Or hasS Or hasF)
End Function
Sub BuildUDFilter()
Dim sql As String
count = 0
If UCase(C_List) = "ALL" Then
sql = " bd.reg IN ( SELECT DISTINCT c FROM att_1 )"
Else
If hasC Then sql = BuildSelect("reg", C_List)
If hasS Then sql = sql & BuildSelect("s", S_List)
If hasF Then sql = sql & BuildSelect("f", F_List)
End If
UDFilter = sql
End Sub
Sub BuildBCSFilters()
Dim sql As String
count = 0
If hasCat Then sql = BuildSelect("sca.cat", cat)
If hasSubcat Then sql = sql & BuildSelect("sca.subcat", subcat)
BCSFilters = sql
End Sub
Private Function BuildSelect(v As String, s As String)
Dim ar As Variant, i As Integer, sql As String
s = UCase(s)
If CBool(InStr(s, "*")) Then
s = Replace(s, "*", "")
sql = " LIKE '%" & s & "%'"
ElseIf CBool(InStr(1, s, "ABC")) Then
s = Left(s, 12)
sql = " LIKE '%" & s & "%'"
Else
ar = Split(s, ",")
For i = 0 To UBound(ar)
ar(i) = Trim(ar(i))
Next
If UBound(ar) = 0 Then
sql = " = '" & ar(0) & "'"
Else
sql = " IN ('" & Join(ar, "','") & "')"
End If
End If
sql = " UPPER(" & v & ")" & sql
If count > 0 Then
sql = vbNewLine & "AND" & sql
End If
count = count + 1
BuildSelect = sql
End Function
Private Function quoted(s) As String
quoted = "'" & s & "'"
End Function
It could be that the line .AdjustColumnWidth = True is contributing to the performance drop? (as it has to load the data to determine auto widths).
Have you considered performing the majority of the code with Application.ScreenUpdating set to False and Application.Calculation set to xlCalculationManual?
For details, see https://www.microsoft.com/en-us/microsoft-365/blog/2009/03/12/excel-vba-performance-coding-best-practices/
This may be worth a try to see if it improves performance. If it does:
You could them put some appropriate user display messages in places for the duration of time that the screen updating is disabled.
Good practice is to store and then restore values for ScreenUpdating and Calculation, so that the environment is left as it was found at the beginning of your subroutine
I'm trying to resolve domain controller name and user(dc\user) using email address.
I have below code(borrowed) but it only solves user name for default domain. Any suggestions much appreciated.
Function Get_LDAP_User_Properties(strObjectType, strSearchField, strObjectToGet, strCommaDelimProps)
If InStr(strObjectToGet, "\") > 0 Then
arrGroupBits = Split(strObjectToGet, "\")
strDC = arrGroupBits(0)
strDNSDomain = strDC & "/" & "DC=" & Replace(Mid(strDC, InStr(strDC, ".") + 1), ".", ",DC=")
strObjectToGet = arrGroupBits(1)
Else
' Otherwise we just connect to the default domain
Set objRootDSE = GetObject("LDAP://RootDSE")
strDNSDomain = objRootDSE.Get("defaultNamingContext")
End If
strBase = "<LDAP://" & strDNSDomain & ">"
' Setup ADO objects.
Set adoCommand = CreateObject("ADODB.Command")
Set ADOConnection = CreateObject("ADODB.Connection")
ADOConnection.Provider = "ADsDSOObject"
ADOConnection.Open "Active Directory Provider"
adoCommand.ActiveConnection = ADOConnection
' Filter on user objects.
'strFilter = "(&(objectCategory=person)(objectClass=user))"
strFilter = "(&(objectClass=" & strObjectType & ")(" & strSearchField & "=" & strObjectToGet & "))"
' Comma delimited list of attribute values to retrieve.
strAttributes = strCommaDelimProps
arrProperties = Split(strCommaDelimProps, ",")
' Construct the LDAP syntax query.
strQuery = strBase & ";" & strFilter & ";" & strAttributes & ";subtree"
adoCommand.CommandText = strQuery
' Define the maximum records to return
adoCommand.Properties("Page Size") = 100
adoCommand.Properties("Timeout") = 30
adoCommand.Properties("Cache Results") = False
' Run the query.
End Function
Thanks in advance
Michal
This should be the code you need to get what you want.
It takes an email address and returns the attributes listed in strAttributes of the requested user.
Example:
Input: LdapUserByMailAddress("ad.user#ad.example.com")
Output: sn: Doe; givenName: John; mail: ad.user#ad.example.com;
Value of strBaseDn: <LDAP://dc=ad,dc=example,dc=com>
Public Function LdapUserByMailAddress(strMailAddress As String) As String
Dim arrMailParts() As String
Dim strUsername As String
Dim strDomain As String
Dim strBaseDn As String
Dim strFilter As String
Dim strQuery As String
Dim strAttributes As String
Dim arrAttributes() As String
Dim i As Integer
Dim j As Integer
strAttributes = "mail,sn,givenName"
arrAttributes = Split(strAttributes, ",")
arrMailParts = Split(strMailAddress, "#")
If 1 <> UBound(arrMailParts) Then
LdapUserByMailAddress = "Not a valid email address"
Exit Function
End If
strUsername = arrMailParts(0)
strDomain = arrMailParts(1)
strBaseDn = "<LDAP://dc=" & Replace(strDomain, ".", ",dc=") & ">"
strFilter = "(sAMAccountName=" & strUsername & ")"
' Construct the LDAP syntax query.
strQuery = strBaseDn & ";" & strFilter & ";" & strAttributes & ";subtree"
Set adoCommand = CreateObject("ADODB.Command")
Set ADOConnection = CreateObject("ADODB.Connection")
ADOConnection.Provider = "ADsDSOObject"
ADOConnection.Open "Active Directory Provider"
adoCommand.ActiveConnection = ADOConnection
adoCommand.CommandText = strQuery
adoCommand.Properties("Page Size") = 100
adoCommand.Properties("Timeout") = 30
adoCommand.Properties("Cache Results") = False
Set resultSet = adoCommand.Execute
LdapUserByMailAddress = ""
For i = 0 To resultSet.Fields.Count - 1
LdapUserByMailAddress = LdapUserByMailAddress & resultSet.Fields(i).Name & ": "
If resultSet.Fields(i).Type = adVariant And Not (IsNull(resultSet.Fields(i).Value)) Then
' For Multi Value attribute.
LdapUserByMailAddress = LdapUserByMailAddress & "[MultiValue]"
For j = LBound(resultSet.Fields(i).Value) To UBound(resultSet.Fields(i).Value)
LdapUserByMailAddress = LdapUserByMailAddress & resultSet.Fields(i).Value(j) & " # "
Next j
Else
' For Single Value attribute.
LdapUserByMailAddress = LdapUserByMailAddress & resultSet.Fields(i).Value
End If
LdapUserByMailAddress = LdapUserByMailAddress & ";"
Next i
End Function
Please elaborate about the given email addresses and add examples about the parameters strObjectType, strSearchField, strObjectToGet and strCommaDelimProps when calling the function Get_LDAP_User_Properties().
What is the expected result?
What is the actual result?
My interpretation of your question:
Your input is an email address like myuser#dc.example.com and you want to get following result: dc.example.com\myuser
Is this correct?
In that case this could be the solution:
Public Function LdapUserByMailAddress(strMailAddress As String) As String
Dim arrParts() As String
arrParts = Split(strMailAddress, "#")
If 1 <> UBound(arrParts) Then
LdapUserByMailAddress = "Not a valid email address"
Exit Function
End If
LdapUserByMailAddress = arrParts(1) & "\" & arrParts(0)
End Function
If you call this function in your worksheet with following code:
=LdapUserByMailAddress("user#dc.example.com")
you get this result: dc.example.com\user
I have used hyperlinks.add several times now and never had any problems with it.
Now I added a line of code: SourceBook.Sheets(ESN & "_SV" & SV).Hyperlinks.Add Anchor:=Range("A" & i), _
Address:=ToPath & NewName to my base code (which you can find under here). This should add a link to the newly created document.
The problem is that excel always says it cannot open the file. The link I enter via code is right, as I copied it with debug.print and it opened the file without a problem.
It came to my attention that the hyperlink I added was modified by excel when I hold my mouse over the hyperlink. I wonder how this is possible.
A second problem I encounterd is that when I enter the hyperlink manually and navigate manually to the file to make sure it takes the right file, excel still modifies my link and says "cannot open specified file".
Anyone an idea what might go wrong here? Thanks!
Code:
`Application.ScreenUpdating = False
Dim i, j, FSO As Object, SV, ESN, PartName, ToPath, FromPath, NewName, MsgBoxAnswer, TargetBook As Workbook, SourceBook As Workbook
Dim OS, PN, SN, ProjectNumber, Customer, StartDate, EndDate, LastRowCMM
ESN = ActiveWorkbook.ActiveSheet.Range("G2").Value
SV = ActiveWorkbook.ActiveSheet.Range("K2").Value
ProjectNumber = ActiveWorkbook.ActiveSheet.Range("A3").Value
Customer = ActiveWorkbook.ActiveSheet.Range("G3").Value
Set FSO = CreateObject("scripting.filesystemobject")
PGB.Min = 0
PGB.Value = 0
PGB.Max = 22
'Create main folder
If SV <> 1 Then
SV = "(SV " & SV & ")"
ToPath = "U:\tmo\vanmolle\fiches constat\Fiches constats #" & ESN & " " & SV
Else
ToPath = "U:\tmo\vanmolle\fiches constat\Fiches constats #" & ESN
End If
If FSO.folderexists(ToPath) = True Then
MsgBoxAnswer = MsgBox("Folder already created.", vbExclamation, "Folder exists.")
Exit Sub
End If
FSO.createfolder (ToPath)
'Create all Excel files & fill them in
For i = 6 To 27
FromPath = "U:\tmo\VANMOLLE\Fiches constat\Template fiches constat LEAP.xlsm"
If SV <> 1 Then
ToPath = "U:\tmo\vanmolle\fiches constat\Fiches constats #" & ESN & " " & SV & "\"
Else
ToPath = "U:\tmo\vanmolle\fiches constat\Fiches constats #" & ESN & "\"
End If
FSO.copyfile Source:=FromPath, Destination:=ToPath
NewName = "#" & ESN & "_" & ActiveWorkbook.ActiveSheet.Range("A" & i) & ".xlsm"
If SV <> 1 Then
FromPath = "U:\tmo\vanmolle\fiches constat\Fiches constats #" & ESN & " " & SV & "\Template fiches constat LEAP.xlsm"
Else
FromPath = "U:\tmo\vanmolle\fiches constat\Fiches constats #" & ESN & "\Template fiches constat LEAP.xlsm"
End If
Name FromPath As ToPath & NewName
Set SourceBook = ThisWorkbook
Set TargetBook = Workbooks.Open(ToPath & NewName)
TargetBook.Sheets("Sheet1").Activate
PartName = SourceBook.ActiveSheet.Range("A" & i).Value
OS = SourceBook.ActiveSheet.Range("D" & i).Value
PN = SourceBook.ActiveSheet.Range("B" & i).Value
SN = SourceBook.ActiveSheet.Range("C" & i).Value
If SN = "" Then SN = "N/A"
StartDate = SourceBook.ActiveSheet.Range("G" & i).Value
EndDate = SourceBook.ActiveSheet.Range("H" & i).Value
'check for right CMM
'LastRowCMM = TargetBook.Sheets("Révision CMM").Range("B6").End(xlDown).Row
'For j = 1 To LastRowCMM
'If PartName = TargetBook.Sheets("Révision CMM").Range("A" & j).Value Then ActiveWorkbook.ActiveSheet.Range("A23").Value = ActiveWorkbook.Sheets("Révision CMM").Range("B" & j).Value
'Next j
TargetBook.ActiveSheet.Range("B9").Value = PartName
TargetBook.ActiveSheet.Range("B10").Value = OS
TargetBook.ActiveSheet.Range("B11").Value = "# " & ESN
TargetBook.ActiveSheet.Range("B12").Value = PN
TargetBook.ActiveSheet.Range("B13").Value = SN
TargetBook.ActiveSheet.Range("E9").Value = StartDate
TargetBook.ActiveSheet.Range("E10").Value = EndDate
TargetBook.ActiveSheet.Range("B14").Value = ProjectNumber
TargetBook.ActiveSheet.Range("B15").Value = Customer
TargetBook.ActiveSheet.PageSetup.PrintArea = "$A$1:$E$39"
TargetBook.Close True
'Add hyperlink
SourceBook.Sheets(ESN & "_SV" & SV).Hyperlinks.Add Anchor:=Range("A" & i), _
Address:=ToPath & NewName
Application.Wait (Now + TimeValue("00:00:01"))
Progress.PGB.Value = i - 5
Progress.Lbl.Caption = "File " & i - 5 & " of 22 copied."
Next i
Application.ScreenUpdating = True`
First thing first - declare each variable explicitly. E.g.:
Dim i as Long, j as Long, FSO As Object, SV as String, ESN as String and etc.
The way in your code - Dim i, j, SV, ESN, PartName, ToPath they are declared as variant.
Second thing second - try something really very small to debug further. E.g. write this small piece:
Sub TestMe()
With Worksheets(1)
.Hyperlinks.Add anchor:=.Range("A1"), Address:="C:\Users\UserName\Desktop\test.docx"
End With
End Sub
and check whether it works. If it doesn't, debug further, check whether cells are locked or anything similar.
SOLVED. Solution at bottom!
Hopefully you brainiacs can help me out as I've apparently reached the limit of my programming capabilities.
I am looking for a way to write a VBA sub which duplicates another VBA Sub, but replace the name and another input. The case in details:
I am trying to build an Excel template for the organization, which will allow the users to inport/export data to/from Access databases (.accdb), as the end-users reluctance towards using real databases (as opposed to excel lists) apparently lies in their inability to extract/submit the data to/from Excel, where they are comfortable working with the data.
The challenge is, that users who don't know how to link to Access, for sure don't know anything about VBA code. Hence, I have created a worksheet from which the users selects a database using a file-path, table, password, set filters, define where to copy/insert datasets, fields to import etc. A Macro then handles the rest.
However, I want to create a macro which allows the user to create additional database links. As it is right now, this would require the user to open VBE and copy two macros and change one line of code... but that is a recipe for disaster. So how can I add a button to the sheet that copies the code I have written and rename the macro?
... I was considering if using a function, but cannot get my head around how that should Work.
Does it make sense? Any ideas/ experiences? Is there a completely different way around it that I haven't considered?
I'd really appreciate your inputs - even if this turns out to be impossible.
Edit:
Macro Man, you asked for the code - it's rather long due to all the user input fields, so I was trying to save you Guys for it since the code in and of itself is working fine...
Sub GetData1()
' Click on Tools, References and select
' the Microsoft ActiveX Data Objects 2.0 Library
Dim DBFullName As String
Dim Connect As String, Source As String
Dim Connection As ADODB.Connection
Dim Recordset As ADODB.Recordset
Dim Col As Integer
Dim DBInfoLocation As Range
Dim PW As String
Dim WSforData As String
Dim CellforData As String
Dim FieldList As Integer
'******************************
'Enter location for Database conectivity details below:
'******************************
Set DBInfoLocation = ActiveWorkbook.Sheets("DBLinks").Range("C15:I21")
FieldList = ActiveWorkbook.Sheets("DBLinks").Range("P1").Value
'******************************
' Define data location
WSforData = DBInfoLocation.Rows(4).Columns(1).Value
CellforData = DBInfoLocation.Rows(5).Columns(1).Value
'Set filters
Dim FilField1, FilField2, FilFieldA, FilFieldB, FilFieldC, FilFieldD, FilFieldE, FilOperator1, FilOperator2, FilOperatorA, FilOperatorB, FilOperatorC, FilOperatorD, FilOperatorE, FilAdMth1, FilAdMthA, FilAdMthB, FilAdMthC, FilAdMthD As String
Dim Filtxt1, Filtxt2, FiltxtA, FiltxtB, FiltxtC, FiltxtD, FiltxtE As String
Dim ExtFld1, ExtFld2, ExtFld3, ExtFld4, ExtFld5, ExtFld6, ExtFld7, ExtFld As String
Dim FilCnt, FilCntA As Integer
Dim FilVar1 As String
'Set DB field names
FilField1 = DBInfoLocation.Rows(1).Columns(5).Value
FilField2 = DBInfoLocation.Rows(2).Columns(5).Value
FilFieldA = DBInfoLocation.Rows(3).Columns(5).Value
FilFieldB = DBInfoLocation.Rows(4).Columns(5).Value
FilFieldC = DBInfoLocation.Rows(5).Columns(5).Value
FilFieldD = DBInfoLocation.Rows(6).Columns(5).Value
FilFieldE = DBInfoLocation.Rows(7).Columns(5).Value
'Set filter operators
FilOperator1 = DBInfoLocation.Rows(1).Columns(6).Value
FilOperator2 = DBInfoLocation.Rows(2).Columns(6).Value
FilOperatorA = DBInfoLocation.Rows(3).Columns(6).Value
FilOperatorB = DBInfoLocation.Rows(4).Columns(6).Value
FilOperatorC = DBInfoLocation.Rows(5).Columns(6).Value
FilOperatorD = DBInfoLocation.Rows(6).Columns(6).Value
FilOperatorE = DBInfoLocation.Rows(7).Columns(6).Value
'Run through criteria to find VarType(FilCrit1) (the Dimension data type) for the criteria field and set the appropriate data type for the filter
currentLoad = IIf(IsNumeric(DBInfoLocation.Rows(1).Columns(7).Value), CDbl(FilCrit1), IIf((DBInfoLocation.Rows(1).Columns(7).Value = "True" Or DBInfoLocation.Rows(1).Columns(7).Value = "False"), CBool(FilCrit1), IIf(IsDate(DBInfoLocation.Rows(1).Columns(7).Value), CDate(FilCrit1), CStr(FilCrit1))))
currentLoad = IIf(IsNumeric(DBInfoLocation.Rows(2).Columns(7).Value), CDbl(FilCrit2), IIf((DBInfoLocation.Rows(2).Columns(7).Value = "True" Or DBInfoLocation.Rows(2).Columns(7).Value = "False"), CBool(FilCrit2), IIf(IsDate(DBInfoLocation.Rows(2).Columns(7).Value), CDate(FilCrit2), CStr(FilCrit2))))
currentLoad = IIf(IsNumeric(DBInfoLocation.Rows(3).Columns(7).Value), CDbl(FilCrit3), IIf((DBInfoLocation.Rows(3).Columns(7).Value = "True" Or DBInfoLocation.Rows(3).Columns(7).Value = "False"), CBool(FilCrit3), IIf(IsDate(DBInfoLocation.Rows(3).Columns(7).Value), CDate(FilCrit3), CStr(FilCrit3))))
currentLoad = IIf(IsNumeric(DBInfoLocation.Rows(4).Columns(7).Value), CDbl(FilCrit4), IIf((DBInfoLocation.Rows(4).Columns(7).Value = "True" Or DBInfoLocation.Rows(4).Columns(7).Value = "False"), CBool(FilCrit4), IIf(IsDate(DBInfoLocation.Rows(4).Columns(7).Value), CDate(FilCrit4), CStr(FilCrit4))))
currentLoad = IIf(IsNumeric(DBInfoLocation.Rows(5).Columns(7).Value), CDbl(FilCrit5), IIf((DBInfoLocation.Rows(5).Columns(7).Value = "True" Or DBInfoLocation.Rows(5).Columns(7).Value = "False"), CBool(FilCrit5), IIf(IsDate(DBInfoLocation.Rows(5).Columns(7).Value), CDate(FilCrit5), CStr(FilCrit5))))
currentLoad = IIf(IsNumeric(DBInfoLocation.Rows(6).Columns(7).Value), CDbl(FilCrit6), IIf((DBInfoLocation.Rows(6).Columns(7).Value = "True" Or DBInfoLocation.Rows(6).Columns(7).Value = "False"), CBool(FilCrit6), IIf(IsDate(DBInfoLocation.Rows(6).Columns(7).Value), CDate(FilCrit6), CStr(FilCrit6))))
currentLoad = IIf(IsNumeric(DBInfoLocation.Rows(7).Columns(7).Value), CDbl(FilCrit7), IIf((DBInfoLocation.Rows(7).Columns(7).Value = "True" Or DBInfoLocation.Rows(7).Columns(7).Value = "False"), CBool(FilCrit7), IIf(IsDate(DBInfoLocation.Rows(7).Columns(7).Value), CDate(FilCrit7), CStr(FilCrit7))))
'Set Filter criteria
FilCrit1 = DBInfoLocation.Rows(1).Columns(7).Value
FilCrit2 = DBInfoLocation.Rows(2).Columns(7).Value
FilCrit3 = DBInfoLocation.Rows(3).Columns(7).Value
FilCrit4 = DBInfoLocation.Rows(4).Columns(7).Value
FilCrit5 = DBInfoLocation.Rows(5).Columns(7).Value
FilCrit6 = DBInfoLocation.Rows(6).Columns(7).Value
FilCrit7 = DBInfoLocation.Rows(7).Columns(7).Value
'Set additional filter-method
FilAdMth1 = DBInfoLocation.Rows(1).Columns(8).Value
FilAdMthA = DBInfoLocation.Rows(3).Columns(8).Value
FilAdMthB = DBInfoLocation.Rows(4).Columns(8).Value
FilAdMthC = DBInfoLocation.Rows(5).Columns(8).Value
FilAdMthD = DBInfoLocation.Rows(6).Columns(8).Value
'Set which fields to extract
ExtFld1 = DBInfoLocation.Rows(1).Columns(9).Value
ExtFld2 = DBInfoLocation.Rows(2).Columns(9).Value
ExtFld3 = DBInfoLocation.Rows(3).Columns(9).Value
ExtFld4 = DBInfoLocation.Rows(4).Columns(9).Value
ExtFld5 = DBInfoLocation.Rows(5).Columns(9).Value
ExtFld6 = DBInfoLocation.Rows(6).Columns(9).Value
ExtFld7 = DBInfoLocation.Rows(7).Columns(9).Value
'Filter on query
'Only criteria of value type string should have single quotation marks around them
FilCnt = 0
If FilField1 <> "" Then
If VarType(FilCrit1) = vbString Then
Filtxt1 = " WHERE [" & FilField1 & "] " & FilOperator1 & " '" & FilCrit1 & "'"
Else
Filtxt1 = " WHERE [" & FilField1 & "] " & FilOperator1 & " " & FilCrit1
End If
FilCnt = 1
End If
If FilField2 <> "" And FilCnt = 1 Then
If VarType(FilCrit2) = vbString Then
Filtxt2 = " " & FilAdMth1 & " [" & FilField2 & "] " & FilOperator2 & " '" & FilCrit2 & "'"
Else
Filtxt2 = " " & FilAdMth1 & " [" & FilField2 & "] " & FilOperator2 & " " & FilCrit2
End If
FilCnt = 2
End If
'Filter on Dataset
FilCntA = 0
If FilFieldA <> "" Then
If VarType(FilCrit3) = vbString Then
FiltxtA = FilFieldA & " " & FilOperatorA & " '" & FilCrit3 & "'"
Else
FiltxtA = FilFieldA & " " & FilOperatorA & " " & FilCrit3
End If
FilCntA = 1
End If
If FilFieldB <> "" And FilCntA = 1 Then
If VarType(FilCrit4) = vbString Then
FiltxtB = " " & FilAdMthA & " " & FilFieldB & " " & FilOperatorB & " '" & FilCrit4 & "'"
Else
FiltxtB = " " & FilAdMthA & " " & FilFieldB & " " & FilOperatorB & " " & FilCrit4
End If
FilCntA = 2
End If
If FilFieldC <> "" And FilCntA = 2 Then
If VarType(FilCrit5) = vbString Then
FiltxtC = " " & FilAdMthB & " " & FilFieldC & " " & FilOperatorC & " '" & FilCrit5 & "'"
Else
FiltxtC = " " & FilAdMthB & " " & FilFieldC & " " & FilOperatorC & " " & FilCrit5
End If
FilCntA = 3
End If
If FilFieldD <> "" And FilCntA = 3 Then
If VarType(FilCrit6) = vbString Then
FiltxtD = " " & FilAdMthC & " " & FilFieldD & " " & FilOperatorD & " '" & FilCrit6 & "'"
Else
FiltxtD = " " & FilAdMthC & " " & FilFieldD & " " & FilOperatorD & " " & FilCrit6
End If
FilCntA = 4
End If
If FilFieldE <> "" And FilCntA = 4 Then
If VarType(FilCrit7) = vbString Then
FiltxtE = " " & FilAdMthD & " " & FilFieldE & " " & FilOperatorE & " '" & FilCrit7 & "'"
Else
FiltxtE = " " & FilAdMthD & " " & FilFieldE & " " & FilOperatorE & " " & FilCrit7
End If
FilCntA = 5
End If
' Select Fields to Extract
ExtFld = "*"
If ExtFld1 <> "" Then
ExtFld = "[" & ExtFld1 & "]"
End If
If ExtFld2 <> "" Then
ExtFld = "[" & ExtFld1 & "],[" & ExtFld2 & "]"
End If
If ExtFld3 <> "" Then
ExtFld = "[" & ExtFld1 & "],[" & ExtFld2 & "],[" & ExtFld3 & "]"
End If
If ExtFld4 <> "" Then
ExtFld = "[" & ExtFld1 & "],[" & ExtFld2 & "],[" & ExtFld3 & "],[" & ExtFld4 & "]"
End If
If ExtFld5 <> "" Then
ExtFld = "[" & ExtFld1 & "],[" & ExtFld2 & "],[" & ExtFld3 & "],[" & ExtFld4 & "],[" & ExtFld5 & "]"
End If
If ExtFld6 <> "" Then
ExtFld = "[" & ExtFld1 & "],[" & ExtFld2 & "],[" & ExtFld3 & "],[" & ExtFld4 & "],[" & ExtFld5 & "],[" & ExtFld6 & "]"
End If
If ExtFld7 <> "" Then
ExtFld = "[" & ExtFld1 & "],[" & ExtFld2 & "],[" & ExtFld3 & "],[" & ExtFld4 & "],[" & ExtFld5 & "],[" & ExtFld6 & "],[" & ExtFld7 & "]"
End If
' Database path info
PW = DBInfoLocation.Rows(3).Columns(1).Value
' Your path will be different
DBFullName = DBInfoLocation.Rows(1).Columns(1).Value
DBTable = DBInfoLocation.Rows(2).Columns(1).Value
' Open the connection
Set Connection = New ADODB.Connection
Connect = "Provider=Microsoft.ACE.OLEDB.12.0;"
Connect = Connect & "Data Source=" & DBFullName & ";Jet OLEDB:Database Password=" & PW & ";"
Connection.Open ConnectionString:=Connect
' Create RecordSet & Define data to extract
Set Recordset = New ADODB.Recordset
With Recordset
'Get All Field Names by opening the DB, extracting a recordset, entering the field names and closing the dataset
Source = DBTable
.Open Source:=Source, ActiveConnection:=Connection
For ColH = 0 To Recordset.Fields.Count - 1
ActiveWorkbook.Worksheets("DBLinks").Range("A1").Offset(ColH + 3, FieldList - 1).Cells.Clear
ActiveWorkbook.Worksheets("DBLinks").Range("A1").Offset(ColH + 3, FieldList - 1).Value = Recordset.Fields(ColH).Name
ActiveWorkbook.Worksheets("RangeNames").Range("A1").Offset(ColH + 2, (DBInfoLocation.Rows(1).Columns(2).Value) - 1).Cells.Clear
ActiveWorkbook.Worksheets("RangeNames").Range("A1").Offset(ColH + 2, (DBInfoLocation.Rows(1).Columns(2).Value) - 1).Value = Recordset.Fields(ColH).Name
Next
Set Recordset = Nothing
End With
' Get the recordset, but only extract the field names of those defined in the spreadsheet.
' If no fields have been selected, all fields will be extracted.
Set Connection = New ADODB.Connection
Connect = "Provider=Microsoft.ACE.OLEDB.12.0;"
Connect = Connect & "Data Source=" & DBFullName & ";Jet OLEDB:Database Password=" & PW & ";"
Connection.Open ConnectionString:=Connect
Set Recordset = New ADODB.Recordset
With Recordset
If FilCnt = 0 Then 'No filter
Source = "SELECT " & ExtFld & " FROM " & DBTable
End If
' Filter Data if selected
If FilCnt = 1 Then
Source = "SELECT " & ExtFld & " FROM " & DBTable & Filtxt1
End If
If FilCnt = 2 Then
Source = "SELECT " & ExtFld & " FROM " & DBTable & Filtxt1 & Filtxt2
End If
.Open Source:=Source, ActiveConnection:=Connection
If FilCntA = 1 Then
Recordset.Filter = FiltxtA
End If
If FilCntA = 2 Then
Recordset.Filter = FiltxtA & FiltxtB
End If
If FilCntA = 3 Then
Recordset.Filter = FiltxtA & FiltxtB & FiltxtC
End If
If FilCntA = 4 Then
Recordset.Filter = FiltxtA & FiltxtB & FiltxtC & FiltxtD
End If
If FilCntA = 5 Then
Recordset.Filter = FiltxtA & FiltxtB & FiltxtC & FiltxtD & FiltxtE
End If
'Debug.Print Recordset.Filter
' Clear data
For Col = 0 To Recordset.Fields.Count - 1
If WSforData <> "" Then
ActiveWorkbook.Worksheets(WSforData).Range(CellforData).Offset(0, Col).EntireColumn.Clear
End If
'ActiveWorkbook.Worksheets("DBLinks").Range("A1").Offset(Col + 3, FieldList - 1).Cells.Clear
Next
' Write field names
For Col = 0 To Recordset.Fields.Count - 1
If WSforData <> "" Then
ActiveWorkbook.Worksheets(WSforData).Range(CellforData).Offset(0, Col).Value = Recordset.Fields(Col).Name
End If
Next
' Write recordset
If WSforData <> "" Then
ActiveWorkbook.Worksheets(WSforData).Range(CellforData).Offset(1, 0).CopyFromRecordset Recordset
ActiveWorkbook.Worksheets(WSforData).Columns.AutoFit
End If
End With
' Clear recordset and close connection
Set Recordset = Nothing
Connection.Close
Set Connection = Nothing
End Sub
This piece of the "DBLinks" worksheet is probably also needed for full understanding of the code:
DBLinks user input area for database connectivity
SOLUTION:
I followed the advice to look into VBProject.VBComponents which copied the macro. I created a simple form which asked for the name to use for the macro and the rest of the inputs comes from the relative reference. I will spare you for a full copy of my long and less than graceful code, but the essential of the code are:
In case someone else could benefit from my experience: In the Click-action of the command button on the form:
Private Sub cmdCreateDB_Click()
'Go to Tools, References and add: Microsoft Visual Basic for Applications Extensibility 5.3
Dim VBProj As VBIDE.VBProject
Dim VBComp As VBIDE.VBComponent
Dim CodeMod As VBIDE.CodeModule
Dim LineNum As Long
Const DQUOTE = """" ' one " character
Set VBProj = ActiveWorkbook.VBProject
Set VBComp = VBProj.VBComponents("Module1")
Set CodeMod = VBComp.CodeModule
Dim txtDBLinkName As String
txtDBLinkName = Me.txtDBName
With CodeMod
LineNum = .CountOfLines + 1
.InsertLines LineNum, " Sub " & txtDBLinkName & "()"
LineNum = LineNum + 1
.InsertLines LineNum, " ' Click on Tools, References and select"
LineNum = LineNum + 1
.InsertLines LineNum, " ' the Microsoft ActiveX Data Objects 2.0 Library"
' And then it goes on forever through all the lines of the original code...
' just remember to replace all double quotations with(Without Square brackets):
' [" & DQUOTE & "]
'And it ends up with:
LineNum = LineNum + 1
.InsertLines LineNum, " Set Recordset = Nothing"
LineNum = LineNum + 1
.InsertLines LineNum, " Connection.Close"
LineNum = LineNum + 1
.InsertLines LineNum, " Set Connection = Nothing"
LineNum = LineNum + 1
.InsertLines LineNum, " End Sub"
End With
Unload Me
End Sub
Thank you everyone for your help. - Especially you #findwindow for coming up with the path to a solution.
For the sake of completion, here's how this could be dealt with without metaprogramming.
Problems that boil down to "do the same thing - but..." can often be solved by making the program as generic as possible. All data specific to a single use-case should be passed down from above in a clear manner, allowing the program to be reused.
Let's look at an example of how this could be implemented in order to generate query strings from one or many ranges of varying sizes.
The first step is to group all data that belongs to the concept of a Filter. Since VBA doesn't have object literals, we can use an Array, a Collection or a Type to represent a Filter instead.
Generating the query strings requires distinction between QueryFilters and RecordFilters. Looking at the code, the two variants are similar enough to be handled by a simple Boolean within a single Type.
Option Explicit
Private Type Filter
Field As String
Operator As String
Criteria As Variant
AdditionalMethod As String
ExtractedFields As String
IsQueryFilter As Boolean
FilterString As String
End Type
Now we can use a single variable instead of keeping track of multiple variables to represent a single concept.
One way a Filter can be generated is by using a Range.
' Generates a Filter from a given Range of input data.
Private Function GenerateFilter(ByRef source As Range) As Filter
With GenerateFilter
.Field = CStr(source)
.Operator = CStr(source.Offset(0, 1))
.Criteria = source.Offset(0, 2)
.AdditionalMethod = CStr(source.Offset(0, 3))
.ExtractedFields = CStr(source.Offset(0, 4))
.IsQueryFilter = CBool(source.Offset(0, 5))
.FilterString = GenerateFilterString(GenerateFilter)
End With
End Function
Just as a single concept can be declared as a Type, a group of things can be declared as an Array (or a Collection, Dictionary, ...). This is useful, as it lets us decouple the logic from a specific Range.
' Generates a Filter for each row of a given Range of input data.
Private Function GenerateFilters(ByRef source As Range) As Filter()
Dim filters() As Filter
Dim filterRow As Range
Dim i As Long
ReDim filters(0 To source.Rows.Count)
i = 0
For Each filterRow In source.Rows
filters(i) = GenerateFilter(filterRow)
i = i + 1
Next
GenerateFilters = filters()
End Function
We now have a function that can return an Array of Filters from a given Range - and, as long as the columns are laid down in the right order, the code will work just fine with any Range.
With all of the data in a convenient package, it's easy enough to assemble the FilterString.
' Generates a FilterString for a given Filter.
Private Function GenerateFilterString(ByRef aFilter As Filter) As String
Dim temp As String
temp = " "
With aFilter
If .AdditionalMethod <> "" Then temp = temp & .AdditionalMethod & " "
If .IsQueryFilter Then
temp = temp & "[" & .Field & "]"
Else
temp = temp & .Field
End If
temp = temp & " " & .Operator & " "
If VarType(.Criteria) = vbString Then
temp = temp & "'" & .Criteria & "'"
Else
temp = temp & .Criteria
End If
End With
GenerateFilterString = temp
End Function
The data can then be merged to strings that can be used in queries regardless of how many Filters of either type are present in the specified Range.
' Merges the FilterStrings of Filters that have IsQueryString set as True.
Private Function MergeQueryFilterStrings(ByRef filters() As Filter) As String
Dim temp As String
Dim i As Long
temp = " WHERE"
For i = 0 To UBound(filters)
If filters(i).IsQueryFilter Then temp = temp & filters(i).FilterString
Next
MergeQueryFilterStrings = temp
End Function
' Merges the FilterStrings of Filters that have IsQueryString set as False.
Private Function MergeRecordFilterStrings(ByRef filters() As Filter) As String
Dim temp As String
Dim i As Long
For i = 0 To UBound(filters)
If Not filters(i).IsQueryFilter Then _
temp = temp & filters(i).FilterString
Next
MergeRecordFilterStrings = temp
End Function
' Merges the ExtractedFields of all Filters.
Private Function MergeExtractedFields(ByRef filters() As Filter) As String
Dim temp As String
Dim i As Long
temp = ""
For i = 0 To UBound(filters)
If filters(i).ExtractedFields <> "" Then _
temp = temp & "[" & filters(i).ExtractedFields & "],"
Next
If temp = "" Then
temp = "*"
Else
temp = Left(temp, Len(temp) - 1) ' Remove dangling comma.
End If
MergeExtractedFields = temp
End Function
With all of that done, we can finally plug a single Range in and get the generated strings out. It would be trivial to change filterRange or generate Filters from multiple Ranges.
Public Sub TestStringGeneration()
Dim filters() As Filter
Dim filterRange As Range
Set filterRange = Range("A1:A10")
filters = GenerateFilters(filterRange)
Debug.Print MergeQueryFilterStrings(filters)
Debug.Print MergeRecordFilterStrings(filters)
Debug.Print MergeExtractedFields(filters)
End Sub
TL;DR
Split code to reusable Functions & Subs
Favor sending data as arguments
Avoid hard-coding
Group data that represent a single concept
Use Arrays or other data structures over multiple variables
The script below basically takes a source table and a target table and copies all the records. It works fairly well, but one recurring issue I'm having is that it seems to truncate some strings. Here is one of the error outputs:
Error Number: -2147217900 Description: [Microsoft][ODBC SQL Server
Driver][SQL S erver]Unclosed quotation mark after the character string
'STRINGSEGMENT^urn:uuid:e9 e91fe151-5w4c-12e1-bac5-25b3a0'.
INSERT INTO TableName VALUES ('23189','23189','','','1^^','','12/5/2013 3:37:2 2
PM','fieldvalue','','somethinghere','somethinghere','12/5/2013 9:37:22
AM','123456','1234568798','STRINGSEGMENT^urn:uuid:
e91fe151-5w4c-12e1-bac5-25b3a0
Query is 584 characters long
If you look at the source data, the string that is truncated looks something like this:
STRINGSEGMENT^urn:uuid:e91fe151-5w4c-12e1-bac5-25b3a0004b00^STRINGSEGMENT
So it's cutting it off after the 53rd character (highlighted). The entire length of tSQL is only 584 characters long.
Why is this happening?
WScript.Echo "Setting Vars..."
Dim sConnect, tConnect, resultSet, r
Dim sDSN, sUserName, sPassWord
Dim tDSN, tUserName, tPassWord
Dim value
sDSN = "mydsn"
sUsername = "myusername"
sPassword = "mypassword"
tDSN = "LOCAL"
tUsername = "myusername"
tPassword = "mypassword"
sTable = "sourceTable"
tTable = "targetTable"
sSQL = "" 'see below
sDSN = "DSN=" & sDSN & ";UID=" & sUsername & ";PWD=" & sPassword & ";"
tSQL = "Select TOP 1 ID FROM " & tTable & " ORDER BY ID Desc"
tDSN = "DSN=" & tDSN & ";UID=" & sUsername & ";PWD=" & sPassword & ";"
Set sConnect = CreateObject("ADODB.Connection")
WScript.Echo "Opening connection to source..."
sConnect.Open sDSN
Set tConnect = CreateObject("ADODB.Connection")
WScript.Echo "Opening connection to target..."
tConnect.Open tDSN
WScript.Echo "Finding Current Record..."
Set r = tConnect.Execute(tSQL)
On Error Resume Next
r.MoveFirst
if r.eof Then currentRecord = 1
Err.Clear
Do While Not r.eof
currentRecord = r("ID") + 1
r.MoveNext
Loop
r.Close
sSQL ="Select * FROM " & sTable & " WHERE ID >= " & currentRecord
WScript.Echo "Beginning shadow at record " & currentRecord & "..."
Set resultSet = sConnect.Execute(sSQL)
resultSet.MoveFirst
Do While Not resultSet.eof
On Error Resume Next
tSQL = "INSERT INTO " & tTable & " VALUES ('"
For i = 0 To resultSet.fields.Count - 1
if NOT IsNull(resultSet(i)) Then
value = replace(resultSet(i),"'","")
'somewhere around here
else
value = ""
End If
tSQL = tSQL & value
if i < resultSet.fields.Count - 1 Then
tSQL = tSQL & "','"
end if
Next
tSQL = tSQL & "')"
'when the error occurs, the line above doesn't seem to be processed but the line below obviously is...
tConnect.Execute(tSQL)
If (Err.Number <> 0) Then
WScript.Echo "Error Number: " & Err.Number & " Description: " & Err.Description
WScript.Echo tSQL
WScript.Echo "Query is " & Len(tSQL) & " characters long"
WScript.StdIn.ReadLine
Err.Clear
End If
tSQL = ""
resultSet.MoveNext
Loop
resultSet.Close
sConnect.Close
Set sConnect = Nothing
tConnect.Close
Set tConnect = Nothing
WScript.Quit(0)
I don't know why this is happening, but here is my workaround. I will not accept this as an answer, just wanted to document.
Function allowed(n)
allowed = true
if n = 13 Then allowed = false
if n = 14 Then allowed = false
End Function
Function sanitize(v,i) 'v=value i=index
mystr = ""
if allowed(i) Then
if Not IsNull(v) Then
mystr = replace(v,"'","")
End If
end if
sanitize = mystr
End Function
Basically I'm just manually excluding the columns that have a problem. Notice that I identified a second one. What's really curious is that columns 12 and 13 have identical data in the source database, but column 12 copies fine.