Required field handling Excel VBA - excel

I have an application I have created in Excel VBA. This application requires a lot of entries from the user. All the fields/cells are required to have values before submission. What will be the best way to check that the users entered values in every single field/cell. I could do simple IF satatements, but that will take forever to write the tons of IFs that I will need. Is there a more efficient way to do it?

You will need to use if statements. You can speed the process up by creating a helper function. This one checks for empty and numeric data only. It can be modified to look for dates and text easily
Function ValidCell(RowPos As Long, ColPos As Long) As Boolean
Dim ws As Worksheet
Dim v As Variant
Set ws = ActiveSheet
v = ws.Cells(RowPos, Colpos).Value
If IsEmpty(v) Or Not IsNumeric(v) Then
ValidCell = False
Else
ValidCell = True
End If
End Function

Related

Table formulas return "Subscript out of range"

I'm trying to enter formulas into tables.
Sometimes I get "Subscript out of range". It doesn't matter how I write the formula, it never works consistently.
You'll see a different formula commented out which doesn't work either.
Sub UpdateAccountTable()
'PURPOSE: Update table data with current data from CW Data Table
'Erik 2022
'
Dim tbl As ListObject
Dim tName As String
Dim warnCol As Long
Dim limitCol As Long
Range("L4").Select
tName = ActiveCell.ListObject.Name 'gets table name
Set tbl = ActiveSheet.ListObjects(tName)
warnCol = tbl.HeaderRowRange.Cells.Find("Current Warn").Column
limitCol = tbl.HeaderRowRange.Cells.Find("Current Limit").Column
' warn and limitcol gets column number because the columns are not always in the same place
StopExcelActions 'function to stop calculate, screen updating ect
With tbl
.ListColumns("warnCol").DataBodyRange.Formula = "=INDEX(CWdata[Warn Value],MATCH([#Helper],CWdata[Helper],0))"
' "=SUMIFS(CWdata[Warn Value], CWdata[Policy Name],[#[Policy Name]],CWdata[Rule Name],[#[Evaluator Description]])"
.ListColumns("limitCol").DataBodyRange.Formula = "=INDEX(CWdata[limit Value],MATCH([#Helper],CWdata[Helper],0))"
' "=SUMIFS(CWdata[Limit Value], CWdata[Policy Name],[#[Policy Name]],CWdata[Rule Name],[#[Evaluator Description]])"
End With
'Range("M4:N4").Select
With tbl.ListColumns("warnCol")
.EntireColumn.Copy
.EntireColumn.xlpastespecial Paste:=xlPasteValues
End With
With tbl.ListColumns("limitCol")
.EntireColumn.Copy
.EntireColumn.xlpastespecial Paste:=xlPasteValues
End With
StartExcelActions
Set tbl = Nothing
FormatData
End Sub
The error:
Consider how your use of `tbl.ListColumns(....) is one of those times where VBA is defaulting to the "Item Property" of the ListColumns object.
So:
ListColumns(StringName) or listColumns(IndexNumber)
are coding variations that point to the "Item" property: ListColumns.Item(variant)
Then notice how you must use the stringName for the column you have, not the String which is the name of the variable you used to store the string. Similarly, if you're using the "indexNumber" version of the object property, you use the value stored in the variable, not the string name of your indexNumber variable.
I give myself a heads up, as I code, and explicitly name all string variables as str_someName, and all indexes as j_someIndexName -- therefore:
Dim str_someName as string
Dim j_someIndexingName as Long
' you might have written Dim jLong_warnCol as Long
If I wrote ListColumns("str_someName") I would see right off that I'm using the string of my variable name, and not the value stored within the string variable. Similarly, if you wrote ListColumns("j_someIndexName"), you might notice that was weird.
Using the naming convention for Long or Integers becomes useful in Loops, so your variables have some expected range of values: Dim long_someName as Long or Dim j_someName' as Integer. You could extend this habit by using "t_..." for table variables, "obj_..." for objects, and on and on. Forced name conventions also helped me learn to avoid coding problems when crafting SQL statements, themselves stored into a str_sql string variable.
The use of OPTION EXPLICIT is an additional check.

Generic function procedure working with different workbooks

I am trying to get better coding practice and using generic function.
I am working with several workbooks from a master file.
For example if I want to get the last row I am using the following line of code.
LastRow=Range("A" & Rows.Count).End(xlUp).Row
To retrieve the value with a function I build the function:
-Function 1
Function GetLastRow() As Integer
GetLastRow = Range("A" & Rows.Count).End(xlUp).Row
End Function
Now from my Sub Main() I want to use GetLastRow() for different workbooks or worksheets. I think it is not a good thing to Activate the workbook before calling my function.
Then should I transmit each time the workbook name and worksheet number to my function and change my function to:
-Function 2
Function GetLastRowIn(sWb As String, iWs As Integer) As Integer
GetLastRowIn = Workbooks(sWb).Worksheets(iWs).Range("A" & Rows.Count).End(xlUp).Row
End Function
Or is there a better/simpler way to transmit the workbook and worksheet in which I want to apply the function while keeping it with no argument as in Function 1?
Thanks for your answers!
To make a function more generic you can allow for some flexibility,
but also impose some rulles for the function calls
Generic Function
Option Explicit
Public Function GetLastRow(ByRef ws As Worksheet, Optional ByVal fromCol As Long = 1) As Long
Dim invalidWS As Boolean, lastRow As Long
If Not ws Is Nothing Then 'check 1st param
On Error Resume Next 'check that ws reference is valid (delted WS invalidates ws)
invalidWS = Len(ws.Name) > 0
invalidWS = Err.Number <> 0 'No error if Err.Number = 0
On Error GoTo 0
If Not invalidWS Then
If fromCol > 0 And fromCol <= ws.Columns.Count Then 'validate 2nd param
lastRow = ws.Cells(ws.Rows.Count, fromCol).End(xlUp).Row
'check if WS.fromCol is empty
If lastRow = 1 And Len(ws.Cells(1, fromCol)) = 0 Then lastRow = 0
End If
End If
End If
GetLastRow = lastRow
End Function
Test Sub
Public Sub TestGetLastRow()
'show results in the Immediate Window (VBA Editor: Ctrl+G)
Debug.Print GetLastRow(Sheet1, 1) 'CodeName of WS
Debug.Print GetLastRow(Workbooks(1).Worksheets(1)) 'WS Index
Debug.Print GetLastRow(ActiveWorkbook.Worksheets("Sheet3"), 3) 'WS name (string)
Debug.Print GetLastRow(ThisWorkbook.Worksheets(1), 0) 'invalid column (or -3)
Dim ws As Worksheet
Set ws = Sheet3
Application.DisplayAlerts = False
ws.Delete 'invalidate ws variable
Application.DisplayAlerts = True
Debug.Print GetLastRow(ws, 1) 'function call with invalid ws object
End Sub
Always use Option Explicit to allow the compiler to catch spelling mistakes in variable names
Validate all input
The function call may not include a valid Worksheet, or column number
Allow the Worksheet to be specified by CodeName, WS Index, or WS Name (string)
Allow a default column ID by using Optional for the 2nd parameter
Impose the call to send only a Worksheet object as the first parameter
If you accept strings for it you need to first check that Worksheet("Invalid") exists
Impose the call to request column by Index
If you allow strings in column ID you need to check that the string is between "A" and "XFD"
String length between 1 and 3, and also not allow strings like "XYZ"
This would require a separate function that checks each letter in the string
Strings also create potential for more maintenance if MS decides to increase max columns
Make the function for one purpose (like you did) - don't include other functionality later on
The function should be self contained
Able to detect and handle all possible errors and unexpected input
And generate the most generic and usable output
By returning a 0 in this particular function, calls that expect a valid number will error out for row 0
So you may want to change it to return 1 even if the sheet is empty
and check the top cell if blank after the call returns
As a note:
several workbooks from a master file
A Workbook is a file
A Worksheet is a tab inside a file (a file can contain multiple sheets / tabs)
Always be explicit with all object
Range("A" & Rows.Count).End(xlUp).Row implicitly uses ActiveWorkbook.ActiveSheet
Translates to ActiveWorkbook.ActiveSheet.Range("A" & Rows.Count).End(xlUp).Row
If you need to work with several Workbooks and Worksheets, fully qualify your calls
`Workbooks("Book1").Worksheets("Sheet3").Range("A" & Rows.Count).End(xlUp).Row
`Workbooks("Book2").Worksheets("Sheet2").Range("A" & Rows.Count).End(xlUp).Row
so Excel can access the exact object you need to work with
If you use full references your code doesn't depend on active objects
- If a user activates a different Workbook, or Worksheet the code will continue to work without errors
Hope this helps
PS. When using row variables always declare them as Long to be able to handle more than the Integer maximum of 32,767 - currently Excel has a max of 1,048,576 rows (in the future this max can increase even higher)

Using cell value as table array value in VLOOKUP (No Macro)

Cell value of B6 = 'Trading Income
=VLOOKUP(B6,'\\myComp.myComp.com\abc\Treas\P&L\data\[DataUS.xls]Smith, Bob'!$A$1:$D$2000,2,FALSE)
This returns (5,555,529.00)
However, say I wanted to place
'\\myComp.myComp.com\abc\Treas\P&L\data\[DataUS.xls]Smith, Bob'!$A$1:$D$2000
in a cell (Lets say B7). How would I structure the VLOOKUP?
I tried:
VLOOKUP(B6, B7, 2, FALSE)
And it returns #N/A
Thank you
Try
=VLOOKUP(B6;'\\myComp.myComp.com\abc\Treas\P&L\data\[DataUS.xls]Smith, Bob'!$A$1:$D$2000;2;FALSE)
You have to use INDIRECT, see this.
In your case, use INDIRECT(B7) instead of B7.
Try this:
=VLOOKUP(B6, INDIRECT(B7), 2, FALSE)
Just ensure that cell B7 contains the exact path i.e. written exactly in the same manner it is written in the formula.
Now adding macros doe
sn't really increase the level of complexity, at least I have not seen any case as yet. On the contrary a macro, if done correctly, increase efficiency,
dependency and certainty of the results.
I suggest this solution which includes macros, provides the simplicity and flexibility of building the link to external data using a Name to hold the External Reference created using user input, which is split in the different parts of the external link to make it easier changes i.e. Path, Filename, Worksheet and Range.
This includes the creation of five Names to handle the linked formula, here is when you might feel like you got it right when mentioning “increasing the level of complexity”, however we can use the power and flexibility of macros not only to produce the expected outcome in a report, analysis, etc.; but also to build forms, reports, graphs, data, etc. and by using macros it also eliminates the need for insanity checks, which at times make us insane, providing and excellent tool to reinstate, review and even change the parameters of large projects when required.
The code provided below includes the creation of the Names, also the refresh the Name that holds the External Link Reference once the user changes any part of the external reference in the worksheet.
First we run this code to create the names (copy this in a module)
Option Explicit
Option Base 1
Sub FmlLnk_WshAddNames()
Const kRowIni As Byte = 2
Const kCol As Byte = 3
Const kWshTrg As String = "Sht(1)"
Dim aNames As Variant
aNames = fNames_Get
Dim WshTrg As Worksheet
Dim bRow As Byte
Dim b As Byte
Set WshTrg = ThisWorkbook.Worksheets(kWshTrg)
With WshTrg
For b = 1 To UBound(aNames)
bRow = IIf(b = 1, kRowIni, 1 + bRow)
.Names.Add Name:=aNames(b), RefersTo:=.Cells(bRow, kCol)
.Names(aNames(b)).Comment = "Name to create link to external range"
Next: End With
End Sub
Function fNames_Get() As Variant
fNames_Get = Array("_Path", "_Filename", "_Worksheet", "_Range")
End Function
Now that the Names to hold the parts of the external link are created we add the worksheet event to automatically update the name holding the External Link Reference (see https://msdn.microsoft.com/EN-US/library/office/ff198331.aspx)
To go to the event procedures for the Worksheet that contains the formula right-click the sheet tab and click “View Code” on the shortcut menu.
Copy the code below in the Worksheet code
Option Explicit
Option Base 1
Private Sub Worksheet_BeforeDoubleClick(ByVal RngTrg As Range, bCancel As Boolean)
Const kFmlLnk As String = "_FmlLnk"
Dim aNames As Variant, vName As Variant
aNames = fNames_Get
Dim WshThs As Worksheet
Dim bLnkExt As Boolean
Dim sLnkExt As String
Set WshThs = RngTrg.Worksheet
With WshThs
Application.Goto .Cells(1), 1
Rem Validate ActiveCell
bLnkExt = False
For Each vName In aNames
If .Names(vName).RefersToRange.Address = RngTrg.Address Then
bLnkExt = True
Exit For
End If: Next
Rem Reset Name Link External
If bLnkExt Then
Rem Built External Formula Link
sLnkExt = "=" & Chr(39) & .Names(aNames(1)).RefersToRange.Value2 & _
"[" & .Names(aNames(2)).RefersToRange.Value2 & "]" & _
.Names(aNames(3)).RefersToRange.Value2 & Chr(39) & Chr(33) & _
.Names(aNames(4)).RefersToRange.Value2
Rem Add External Formula Link Name
.Names.Add Name:=kFmlLnk, RefersTo:=sLnkExt
.Names(kFmlLnk).Comment = "Name to link external range in Formula"
End If: End With
End Sub
This procedure will run every time the users double-clicks in any of the four Names created in the worksheets that holds the External Link Formula
The Formula to use the external link name is:
=VLOOKUP($B9,_FmlLnk,3,0)

writing in excel from access line by line using vba code

Hi guys i am new here and i am new to vba.
i want to solve the following problem:
i have two different access tables. each of them contains data i want to compare first and then, if a certain constraint is true i want to import certain columns out of one of the two access db tables into an excel sheet.
what i already have: the connection to the databases, i can read the data and print them on the console via debug.print command.
i have really no idea how to write certain rows (those which conform to the constraint) to the excel sheet.
Code sample
'commandstring and data base variables stands here
'non database connection variables
Dim oldID, newID, oldBuildPlanned, newBuildPlanned As String
Dim createExcel, doesExcelExist As Boolean
Dim xl As Excel.Application
Dim wb As Excel.Workbook
Dim Wksht As Excel.Worksheet
Dim dataVar As String
Dim counter As Integer
counter = 0
createExcelSheet = False
doesSheetExist = False
'Debug.Print "TEST old database"
Do While Not objRs.EOF And Not objRs2.EOF
'Debug.Print vbTab & objRs(0) & " " & objRs(1)
'assigning database values to variables to make them comparable
oldID = objRs(counter)
newID = CStr(objRs2(counter))
oldBuildPlanned = objRs(counter + 1)
newBuildPlanned = objRs2(counter + 1)
If oldID = newID And oldBuildPlanned = newBuildPlanned Then
createExcel = True
If createExcelSheet = True And Not doesSheetExist = True Then
Set xl = New Excel.Application
Set wb = xl.Workbooks.Add
Set Wksht = ActiveWorkbook.Worksheets("Sheet1")
doesExcelExist = True
End If
Call writeReport(newID)
End If
objRs.MoveNext
objRs2.MoveNext
Loop
'tidy up stuff comes here
end of code
I am sorry if my code is not formatted as its usual, its my first post in this forum ^^
So the writeReport() should contain the code to write the data into the sheet. i planned to insert the id's of the matching database entries into the method as parameters and read these certain data out of the recordset. but i cannot convert recordset items to string, so the byRef parameter declaration causes a compile error "type mismatch". In addition i tried to export the table with DoCmd.TransferSpreadsheet, but this method exports the entire table into excel, it worked, but it is not what i am searching for.
i hope someone can help me with my little low level problem, if you need further information feel free to ask me. i am using ADO.
Thanks in advance
Welcome to the forum,
I think you might find these two websites helpful for what you are trying to do. The first one is a great tutorial on using Access and Excel together.
http://www.datawright.com.au/excel_resources/excel_access_and_ado.htm
http://www.w3schools.com/sql/default.asp
I am not sure how you are creating your recordset, by I would recommend using an SQL statement as your source. That way you only pull the data from Access that you need. If you have any more specific questions, please let me know.
Jason

Macro to find multiple strings and insert text (specific to each string) at the end of each occurrence

The scenario:
Word documents that contain a selection of sentences (strings). There might be up to 30 possible strings (which vary from 5 to 20 words in length). The document will contain only a selection of these strings.
Aim:
Macro that searches through the document, finds each occurrence of a particular string and inserts a specific text code (such as " (ACWD2553)") after each occurrence. This is repeated for all the other strings in the set, with each different string having it's own distinct code. Some strings won't be in the document. The strings can be located in document body and table cells.
The macro would then be applied to other documents which would have different selections of the strings.
I have tried for many days using selection.find, content.find, target.list, insertafter and so on but only with one case and still ran into numerous problems (e.g. only inserting in one instance, or code repeatedly inserting until Word freezes).
Bonus feature ###
Be able to choose which set of strings which will be searched for (there are potentially up to 60 sets) and their corresponding codes. Each document would only have strings from one set.
An idea I had was for the strings to be listed in a column (in Excel?) and the matching codes in the a second column. The macro would then search the document for each string in the list (stopping at the end of the list since the number of strings varies between sets) finds the matching code in the cell in the next column and then inserts the code for each occurrence of the string in the word doc. When a different set is required, the Excel file could be swapped with the file containing the relevant set of stings, but with the same file name. Or all sets in the one Excel file on different worksheets and tab name entered in Word (userform?) which forces search of relevant set. This file would be located on a network drive.
Not sure if this is bigger then Ben Hur, last bit would be nice, but I can also manually enter the strings in the raw code from a template code.
Edited this post to include my poor attempt at the code. See my comment below. I just realised that I could add code to this pane. Tried a variety of iterations of the one below, none of which worked well and which does not approach what I require. I know there are obvious errors, as I said below I have played around with the code and made it worse in the process by mixing bits and pieces together.
Sub Codes()
Dim range As range
Dim i As Long
Dim TargetList
TargetList = Array("This is sentence 1", "This is string 2 which could be twenty words in length", "This is string three, there could be thirty more strings to search") ' put list of terms to find here
For i = 0 To UBound(TargetList)
Set range = ActiveDocument.range
With range.Find
.Text = TargetList(i)
.Format = False
.MatchCase = False
.MatchWholeWord = False
.MatchWildcards = False
.MatchSoundsLike = False
.MatchAllWordForms = False
Do While .Execute(Forward:=True) = True
range.Find.Execute
range.InsertAfter Text:=" (ACWD1234)"
Loop
End With
Next
End Sub
I think that this is a time to use replace rather than find, see implementation below. If the specific code changes depending on the target string you can hanlde this easily with a 2 dimensional array
Sub Codes()
Dim i As Long
Dim TargetList
Dim MyRange As range
TargetList = Array("This is sentence 1", "This is string 2 which could be twenty words in length", "This is string three, there could be thirty more strings to search") ' put list of terms to find here
Dim sStringToAdd As String
sStringToAdd = " (ACWD2553)"
For i = 0 To UBound(TargetList)
Set MyRange = ActiveDocument.Content
MyRange.Find.Execute FindText:=TargetList(i), ReplaceWith:=TargetList(i) & sStringToAdd, _
Replace:=wdReplaceAll
Next i
End Sub
The code below does exactly what you need. I dont know if replacing the whole Contents property of the document object has some weird effect into tabulation/formating and so on.
I'd rather not add any overhead with string/array/collection manipulations. Using find-replace is probably the most obvious route, but I don't like that whole lot of options you need to set (because I understand none of them =P)
You need to add a reference to "Microsoft scripting runtime"
Public Sub changeTokens()
Dim strContents As String
Dim mapperDic As Scripting.Dictionary
Dim thisTokenKey As String
Dim varKey As Variant
Set mapperDic = getTokenMapper()
For Each varKey In mapperDic.Keys
thisTokenKey = CStr(varKey)
ThisDocument.Content = Replace(ThisDocument.Content, thisTokenKey, mapperDic(thisTokenKey))
Next varKey
End Sub
Public Function getTokenMapper() As Scripting.Dictionary
' This function can fetch data from other sources to buidl up the mapping.
Dim tempDic As Scripting.Dictionary
Set tempDic = New Scripting.Dictionary
Call tempDic.Add("Token 1", "Token 1 changed!!")
Call tempDic.Add("Token 2", "Token 1 changed!!")
Call tempDic.Add("Token 3", "Token 1 changed!!")
Set getTokenMapper = tempDic
End Function
You can fetch your data to create the mapper dictionary from a excel worksheet with no problems.
Thanks to the two respondents. I don't have the skillset to progress the second code. I ended up searching for reading data from Excel into a word document and found code that worked perfectly.
Using Excel as data source in Word VBA
http://social.msdn.microsoft.com/Forums/office/en-US/ca9a31f4-4ab8-4889-8abb-a00af71d7307/using-excel-as-data-source-in-word-vba
Code produced by Doug Robbins.
This worked an absolute treat.
Also it means that I can edit the Excel file for the different sets of statements and their matching codes. Now it would be particularly sweet if I could work out a way to create a userform that would open when i run the macro and select the appropriate woprksheet based on the userform dropdown list item selected.

Resources