Using Worksheet.Change event to add a new worksheet - vba

I have a range of cells (specifically D6:D34) where all the values in the cells have a corresponding sheet. However, since I've been just manually adding worksheets when I add a new value (or change a cell value), I'm thinking about using Private Sub Worksheet_Change(ByVal Target as Range) to allow the automatic creation of a worksheet when the cells change. This is what I've tried to use, but now I'm getting an error that the "sheet name already exists" as it looks down the whole column. I've tried using error handling to skip over ones that exist, but it ends up moving to the next one to check but leaving "Sheet1" and "Sheet2", etc. Any suggestions on how to set this up?
Private Sub Worksheet_Change(ByVal Target As Range)
Dim hlValue As Range
For Each hlValue In Sheets(1).Range("D6:D34")
ActiveWorkbook.Sheets.Add after:=Worksheets(Worksheets.Count)
ActiveSheet.Name = hlValue
Next
End Sub
I should also say that if one of the cell value is deleted, the worksheet should be deleted as well. Some sort of If CellValue <> Exist, Delete? I couldn't find anything to use to check if it exists besides fancy functions. Should I use one of these?
EDIT: Okay, I've got this now. This should suffice.
Private Sub Worksheet_Change(ByVal Target As Range)
Application.DisplayStatusBar = True
Application.ScreenUpdating = False 'Run faster
Application.DisplayAlerts = False 'Just in case
Dim shtName As Variant
For Each shtName In Sheets(1).Range("D6:D34")
If WorksheetExists((shtName)) Then
'do nothing
Else
ActiveWorkbook.Sheets.Add after:=Worksheets(Worksheets.Count)
ActiveSheet.Name = shtName
Application.StatusBar = "Creating new sheet for " & shtName 'Just in case it's running slowly
Sheets("Admin").Select
End If
Next
Application.StatusBar = "READY"
Application.ScreenUpdating = True
Application.DisplayAlerts = True
End Sub
Function WorksheetExists(sName As String) As Boolean
WorksheetExists = Evaluate("ISREF('" & sName & "'!A1)")
End Function

I couldn't find anything to use to check if it exists besides fancy functions. Should I use one of these?
Yes, you should! Worksheets are part of a Collection object and there is no built-in Exists (or similar) method that you can query. Such a function is not fancy :) and it would be a good introduction to using functions and/or calling other subroutines, if you're not familiar with that already.
At it's simplest:
Function SheetExists(sName As String) As Boolean
Dim w as Worksheet
On Error Resume Next
Set w = Worksheets(sName)
SheetExists = Not w Is Nothing
End Function
How this works:
If SheetExists("sheet1") Then
'Do something
Else
'Sheet doesn't exist, so do something else
End If
You pass a string value to the function as sName. THe function then returns True or False whether this sheet exists.
First, the function SheetExists attempts to set a Worksheet variable to the specified worksheet, by name. This will predictably fail if the worksheet name doesn't exist, so we use this knowledge along with the Resume Next statement. In the case of an error, w will not be assigned a worksheet and will remain a Nothing, and then we use a boolean expression (Not w Is Nothing) as the function's return value. If the sheet does exist, w will not be nothing, so the function will return True, and if the sheet doesn't exist, w will be Nothing, so the function will return False.
The function above only uses the ActiveWorkbook, so a more robust version of this would also allow you to specify a parent workbook.
Function SheetExists(sName As String, Optional wb as Workbook = Nothing) As Boolean
'This function checks whether worksheet 'sName' exists in
' workbook object 'wb'. If no parameter is passed for 'wb' then
' assume to use the ActiveWorkbook
Dim w as Worksheet
If wb Is Nothing Then Set wb = ActiveWorkbook
On Error Resume Next
Set w = wb.Worksheets(sName)
SheetExists = Not w Is Nothing
End Function
NB: There are relatively few cases where On Error Resume Next is not frowned upon, but using this in a very small and specific Function, with a well-defined purpose and expectation is OK.
Alternatively, brute force iteration over the collection's Items may also be used to query collections for existence, and this does not rely on On Error Resume Next:
Function SheetExists2(sName as String) As Boolean
Dim ws as Worksheet, ret as Boolean
For Each ws In ActiveWorkbook.Worksheets
If ws.Name = sName Then
ret = True
Exit For
End If
Next
SheetExists2 = ret
End Function

Related

VBA create new Worksheet if not exists

I am currently working on a VBA macro, that takes data from a Worksheet and copies it to another.
If the destination Worksheet does not exist it should create it and than write the data from my array.
Problem:
I have a function to test if the worksheet already exists.
If it is the case my macro will successfully write the data i want. But if the worksheet doesnt exist VBA is displaying the error you can see below.
In the list Workbook.Worksheets is no Sheet named like this but I get that error anyway.
Here is my relevant code:
(If something is missing for understanding the problem I can fill in the missing part in too)
Function sheetExists(sheetToFind As String) As Boolean
Dim Sheet As Worksheet
For Each Sheet In Worksheets
If sheetToFind = Sheet.Name Then
sheetExists = True
Exit Function
End If
Next Sheet
sheetExists = False
End Function
In my main Sub I used this code:
If sheetExists("SheetName") = False Then
Dim newSheet As Worksheet
With ThisWorkbook
.Sheets.Add(After:=Worksheets(Worksheets.Count)).Name = "SheetName"
End With
End If
The exact error:
1004 Cannot rename a sheet to the same name as another sheet, a reference object library, or a workbook referenced by Visual Basic
First it was executing successfully but after I deleted the sheet manually the error occurred.
Thanks for any help :)
Specify in which workbook to look at:
For Each Sheet In ThisWorkbook.Sheets
also not that it has to be Sheets and not Worksheets, because Worksheets only contains worksheets but Sheets also contains charts, etc. So we have to check these names too!
(Sheet then has to be Dim Sheet As Object)
You can make your function more flexible:
Function sheetExists(sheetToFind As String, Optional InWorkbook As Workbook) As Boolean
If InWorkbook Is Nothing Then Set InWorkbook = ThisWorkbook
Dim Sheet As Object
For Each Sheet In InWorkbook.Sheets
If sheetToFind = Sheet.Name Then
sheetExists = True
Exit Function
End If
Next Sheet
sheetExists = False
End Function
so you can call it:
sheetExists("SheetName") to use ThisWorkbook by default, or
sheetExists("SheetName", Workbooks("MyWorkbook")) to specify a specific workbook.
Alternatively you can use
Function sheetExists(sheetToFind As String, Optional InWorkbook As Workbook) As Boolean
If InWorkbook Is Nothing Then Set InWorkbook = ThisWorkbook
On Error Resume Next
sheetExists = Not InWorkbook.Sheets(sheetToFind) Is Nothing
End Function
which can be a bit faster if there are many sheets in a workbook.

VBA Code for Vlookup on different worksheets within the same workbook

I am trying to write a vba script that will allow me to vlookup values from Sheet(3) to different Sheet(i) - and paste it on range"R2" on the Sheet(i) - I also want it to go to the end of the values in Column M on Sheet(i) [if this is possible]. I basically want to run through all the different "i" sheets on the workbook. Sheet (3) has all the data that needs to be copied on all the other "i" sheets.
I keep getting an error with my code below.
Sub CopyTableau1Data()
Dim wka As Worksheet
Dim wkb As Worksheet
ShtCount = ActiveWorkbook.Sheets.Count
For i = 9 To ShtCount
With ThisWorkbook
Set wka = .Sheets(i)
Set wkb = .Sheets(3)
End With
Worksheets(i).Activate
If IsError(Application.WorksheetFunction.VLookup(wka.Range("M2"), wkb.Range("E:T"), 14, 0)) Then
wka.Range("R2").Value = ""
Else
wka.Range("R2").Value = Application.WorksheetFunction.VLookup(wka.Range("M2"), wks.Range("E:T"), 14, 0)
End If
Next i
End Sub
IsError does not work with Application.WorksheetFunction.VLookup or WorksheetFunction.VLookup, only with Application.VLookup.
It is faster and easier to return Application.Match once to a variant type variable and then test that for use.
dim am as variant
'are you sure you want wkb and not wks here?
am = Application.match(wka.Range("M2"), wkb.Range("E:E"), 0)
If IsError(am) Then
wka.Range("R2") = vbnullstring
Else
'are you sure you want wks and not wkb here?
wka.Range("R2") = wks.cells(am, "R").value
End If
Note the apparent misuse of wkb and wks in two places. I don't see the point of looking up a value in one worksheet, testing that return then using the results of the test to lookup the same value in another worksheet.
You can use the following code:
Sub CopyTableau1Data()
Dim wka As Worksheet
Dim wkb As Worksheet, i As Integer
ShtCount = ActiveWorkbook.Sheets.Count
For i = 9 To ShtCount
With ThisWorkbook
Set wka = .Sheets(i)
Set wkb = .Sheets(3)
End With
Worksheets(i).Activate
wka.Range("R2") = aVL(i)
Next i
End Sub
Function aVL(ByVal wsno As Integer) As String
On Error GoTo errhandler:
aVL =
Application.WorksheetFunction.VLookup(ActiveWorkbook.Worksheets(wsno).Range("M2"),
ActiveWorkbook.Worksheets(3).Range("E:T"), 14, False)
errhandler:
aVL = ""
End Function
When you try to check an error by isError, program flow can immediately return from the sub depending on the error. You could use on error in your sub to prevent that but this would only handle the first error. By delegating the error handling to a function you can repeatedly handle errors in your loop.
I assumed you wanted to use wkb.Range("E:T") instead of wks.Range("E:T").

Selecting first empty row with specific columns for each sheet VBA

Is there any way that I can possibly make the function change to a specific column for each sheet in the ActiveWorkbookI tried various versions but can't seem to get it right.
Sub resetFilters()
Dim sht As Worksheet
On Error GoTo ErrorHandler
Application.ScreenUpdating = False
'On Error Resume Next
If ActiveSheet.FilterMode Then
ActiveSheet.ShowAllData
End If
Range("A3:T3").ClearContents
Application.ScreenUpdating = True
Call GetLastRow
Exit Sub
ErrorHandler:
Debug.Print "Error number: " & Err.Number & " " & Err.Description
End Sub
Private Function SelectFirstEmptyRowInColumnH(ByVal sheet As Worksheet, Optional ByVal fromColumn As Long = 8) As Long
SelectFirstEmptyRowInColumnH = sheet.Cells(sheet.Rows.Count, fromColumn).End(xlUp).Row
End Function
Private Sub GetLastRow()
Dim selectLastRow As Long
selectLastRow = SelectFirstEmptyRowInColumnH(ActiveSheet, 8)
Cells(selectLastRow, 8).Offset(1, 0).Select
End Sub
A worksheet is an object and can't be passed as an argument ByVal. It must be ByRef, which is the default and can therefore be omitted. Note also that Sheet is a word reserved for VBA's use. In most cases VBA will be able to determine your intention and allow you to use its vocabulary the way you wish, but for you, when faced with the task of trouble shooting, it is a hell of a job to determine in each case whether Sheet means VBA's sheet or your own sheet. Select any word in your code and press F1 to let VBA show you the meaning it attaches to it and how to use it.
Other than that, note that your function returns the last used row. The first empty one is the next one after that. So, I would write that function somewhat like this:-
Private Function FirstEmptyRow(Ws As Worksheet, _
Optional ByVal Clm As Long = 1) As Long
With Ws
FirstEmptyRow = .Cells(.Rows.Count, Clm).End(xlUp).Row + 1
End With
End Function
Observe that I changed the default for the optional column to 1. The default should be both the most logical choice and the one most commonly used. In the case of the last row that is the first column, column A.
Here is an alternative based on your comment (which I couldn't fully understand). This code looks for the word "Style" in Rows(3) of the ActiveSheet and returns the next blank row in the column where "Style" was found.
Private Function FirstEmptyRow() As Long
' 9 Apr 2017
Dim Clm As Long
With ActiveSheet
On Error GoTo ErrHandler:
Clm = WorksheetFunction.Match("Style", .Rows(3), 0)
FirstEmptyRow = .Cells(.Rows.Count, Clm).End(xlUp).Row + 1
End With
ErrHandler:
Err.Clear
End Function
If the word "Style" isn't found an error will occur and the execution will jump to the Label ErrHandler: which does nothing. You might want to let it handle the situation in some way. As the function stands the row number it returns will be zero which will cause an error if you try to address that row.
you could use this function:
Private Function SelectFirstEmptyRowInColumnWithGivenHeader(ByVal sheet As Worksheet, Optional ByVal header As String = "Style") As Long
Dim col As Variant
With sheet
col = Application.Match(header, .Rows(1), 0)
If Not IsError(col) Then
.Activate '<--| you must select a sheet to activate a cell of it
.Cells(.Rows.Count, col).End(xlUp).Offset(1).Select
End If
End With
End Function
and exploit it in your main code as follows:
Sub main()
Dim sht As Worksheet
Application.ScreenUpdating = False '<--| this to prevent sheet activating slow down the code (and annoy you)
For Each sht In Worksheets
SelectFirstEmptyRowInColumnWithGivenHeader sht , "Style" '<--| you can omit the 2nd parameter and it'll be assumed the default column header
Next
Application.ScreenUpdating = True '<--| get default behavior back in place
End Sub
You can just pass the desired column number to the function. Optional ByVal fromColumn As Long = 8 means that column 8 (column H) is the default column if no column number is passed when the function is called. But passing a column number will override that default.
So in this line, passing the 8 is actually not required, although probably good for clarity, and could be written like so with the same result (returning the last row for column H):
selectLastRow = SelectFirstEmptyRowInColumnH(ActiveSheet)
To change the column number to 2 (column B) for example, you would change the line like so:
selectLastRow = SelectFirstEmptyRowInColumnH(ActiveSheet, 2)
I would also recommend that you genericize the name of the function to SelectFirstEmptyRowInColumn so to avoid confusion.
This simple code will help you.
Sub FindFirstEmptyRow()
Cells(Rows.Count, 1).End(xlUp).Offset(1).Select
End Sub
☺ ☺

returning object that caused error to a global errorhandler

i have written the following code which uses a global errorhandler that rectifies the error and resumes code again. right now i have labelled the lines and using 'Erl', i am checking which line caused the error and then accordingly finding the object and creating it.
What i want to do is to pass the object that caused the error to the errorhandler itself and then create that object and return control back to main procedure to resume from where it left off.
How to return the object that caused the error to an error handler? this way it would be more appropriate to handle some errors, instead of just returning an error message and error description.
as an example, i am letting the errorhandler handle creation of sheets if they are non-existent.
Option Explicit
'Worksheets: Public to VBproject as it may be used in almost every module.
Public WSCombined As Worksheet
Public WSResult As Worksheet
'Worksheet names: VBPublic to project as it may be used in almost every module.
Public Const Combined As String = "Combined data"
Public Const Result As String = "Result"
'Array: Public to VBproject as it may be used in almost every module.
Public Arr() As Variant
Sub Main()
On Error GoTo ERRHANDLER
Dim Rng As Range
With ThisWorkbook
'error labels for passing to Erl, so that corresponding sheet names can then be selected for sheet objects.
10001: Set WSCombined = .Sheets(Combined)
10002: Set WSResult = .Sheets(Result)
10003: Set Rng = WSCombined.Rows(1).EntireRow
10004: Arr = Rng
End With
EXITSUB:
' cleanup
Set WSCombined = Nothing
Set WSResult = Nothing
Exit Sub
ERRHANDLER:
If ErrorHandling(Err, ThisWorkbook, Erl) Then
GoTo EXITSUB
Else
Resume
End If
End Sub
Function ErrorHandling(objError As Object, Optional WB As Workbook, Optional ERlCode As Long) As Boolean
With objError
Select Case .Number
Case Is = 9 'subscript out of range
Select Case ERlCode
Case Is = 10001 ' Worksheet: for Combined sheet
Call SheetExists(WB, Combined)
Case Is = 10002 ' Worksheet: for Result sheet
Call SheetExists(WB, Result)
Case Is = 10003 ' Array: Array not initialized
Call IsArrayAllocated(Arr)
Case Else
End Select
ErrorHandling = False ' resume again at same line, after sheet has been created.
Case Is = 91 ' Object variable or with variable not found
Call IsArrayAllocated(Arr)
Case Is = ""
Case Else
MsgBox objError.Number & " - " & objError.Description
ErrorHandling = True
End Select
End With
objError.Clear
On Error GoTo 0
End Function
Function SheetExists(WBook As Workbook, SHTName As String) As Worksheet
On Error GoTo ERRHANDLE
With WBook
Set SheetExists = .Sheets(SHTName)
End With
EXITFUNC:
Exit Function
ERRHANDLE:
With WBook
Dim Sh As Worksheet
Set SheetExists = .Sheets.Add
SheetExists.Name = SHTName
End With
Resume EXITFUNC
End Function
Function IsArrayAllocated(Arr As Variant) As Variant
'Courtesy: #Chip Pearson
' check if array is initialized, if not initialize it
On Error GoTo ERRHANDLE
IsArrayAllocated = (IsArray(Arr) And Not IsError(LBound(Arr, 1)) And LBound(Arr, 1) <= UBound(Arr, 1))
EXITFUNC:
Exit Function
ERRHANDLE:
ReDim Arr(0 To 0)
IsArrayAllocated = Arr
GoTo EXITFUNC
End Function
To tackle some errors, the other way i was thinking is to just have functions created which could have their own error handling code and rectify the error in place itself, instead of passing it to a global error handler of main proc.
e.g.
Sub Main()
On Error GoTo ERRHANDLER
Dim Rng As Range
With ThisWorkbook
set WSCombined = SheetExists(ThisWorkbook, Combined)
Set WSResult = SheetExists(ThisWorkbook, Result)
....
....
Function SheetExists(WBook As Workbook, SHTName As String) As Worksheet
On Error GoTo ERRHANDLE
With WBook
Set SheetExists = .Sheets(SHTName)
End With
EXITFUNC:
Exit Function
ERRHANDLE:
With WBook
Dim Sh As Worksheet
Set SheetExists = .Sheets.Add
SheetExists.Name = SHTName
End With
Resume EXITFUNC
End Function
Any help would be most appreciated.
One answer to your specific question is to use another global object to store the object that threw the error in:
Public LastErrorObject As Object
Sub Main()
With ThisWorkbook
Set LastErrorObject = ThisWorkbook
Set WSCombined = .Sheets(Combined)
End With
End Sub
However, this is incredibly cumbersome, because you need to keep track of it everywhere. Also, it still doesn't do much in the way of giving you the context for the error.
Your second idea is much better, and doesn't abuse the error handler as much because it both acknowleges that there may be an error and actively seeks to avoid errors instead of passively trying to deal with them. For example, instead of indexing into ActiveWorkbook.Worksheets() directly, you can do something like this to make sure you get a new sheet back:
Private Function GetOrCreateWorksheet(book As Workbook, name As String) As Worksheet
On Error GoTo ForceNew
Dim result As Worksheet
Set result = book.Worksheets(name)
ForceNew:
If Err.Number = 9 Then
Set result = book.Worksheets.Add()
result.name = name
ElseIf Err.Number <> 0 Then
Err.Raise Err.Number, Err.source, Err.Description
End If
Set GetOrCreateWorksheet = result
End Function
Note that the error handling is only checking for the one specific error that you'll get if the sheet doesn't exit (Subscript out of range).
BTW and off topic, your Select Case syntax is overly convoluted - you only need to specify the case and can leave out the Is = syntax. It is also much easier to read with another level of indentation:
Select Case ERlCode
Case 10001
'...
Case 10002
'...
Case Else
'...
End Select

Comparing two Sheet objects (not contents)

In the context of an error handling code, I would like to verify if the user has given to the current sheet the same name of another one into the same workbook (action forbidden, of course). So the way I intuitively tried to verify this was simply to loop through all the sheets and comparing the names:
For Each sh In ThisWorkbook.Sheets
If sh.Name = ThisWorkbook.ActiveSheet.Name Then
'error handling here
End If
Next sh
However, this is a huge logic fall in the case when:
1) The user is editing, let's say, the sheet number 3;
2) The sheet with the same name is at the position number 5;
In that case, the condition sh.Name = ThisWorkbook.ActiveSheet.Name would be met for sure because the sheet is compared to itself.
So, I wonder: how to understand if sh is not ThisWorkbook.ActiveSheet?
I had thought the task it could have simply been solved with a simple object comparison:
If (sh Is Not ThisWorkbook.ActiveSheet) And (sh.Name = ThisWorkbook.ActiveSheet.Name) Then
but this raises a compile error, namely Object does not support this property or method. Could anyone please help me finding the lack of logic in my code's structure?
OTHER INFORMATION
I have tried to manage the case through the Err.Description and the Err.Number, but the first is OS-language dependent and the second is the same for other types of error I need to handle differently.
Moreover, the sheets (names and contents) are contained into a .xlam add-in so the user can change the contents through custom user-forms but not through the Excel Application.
More in general, let's say that I would like to know how can I perform the comparison, even if a work-around in this specific case is possible, in order to use this method for future developments I already plan to do and that cannot be managed through the default VBA error handler.
Just check the index of the worksheet along with the name.
Only error (or whatever) if the name matches, but the index doesn't.
Option Explicit
Public Sub test()
Dim wb As Workbook
Dim ws As Worksheet
Set wb = ThisWorkbook
Set ws = wb.ActiveSheet
Dim wsToCheck As Worksheet
For Each wsToCheck In wb.Worksheets
If ws.Name = wsToCheck.Name And ws.Index <> wsToCheck.Index Then
'do something
End If
Next
End Sub
Of course, you could always just test for object equality using the Is operator too, or inequality in your specific case.
Public Sub test2()
Dim wb As Workbook
Dim ws As Worksheet
Set wb = ThisWorkbook
Set ws = wb.ActiveSheet
Dim wsToCheck As Worksheet
For Each wsToCheck In wb.Worksheets
If Not ws Is wsToCheck Then
'do something
Debug.Print ws.Name
End If
Next
End Sub
You've got an incorrect syntax with "Not"; it should be this:
If (Not sh Is ThisWorkbook.ActiveSheet) And (sh.Name = ThisWorkbook.ActiveSheet.Name) Then
There's no reason to loop through the collection of sheets. Use this:
Function IsWshExists(ByVal wbk As Workbook, ByVal wshName As String) As Boolean
Dim wsh As Worksheet
On Error Resume Next
Set wsh = wbk.Worksheets(wshName)
IsWshExists = (Err.Number = 0)
Set wsh = Nothing
End Function
Usage:
If Not IsWshExists(ThisWorkbook, "Sheet2") Then
'you can add worksheet ;)
'your logic here
End If