Find function within a Do Loop providing inconsistent results - vba

I am trying to test the differences between two lists. My VBA code is supposed to loop through list one and use the find method to look at each item in list two. If the item is not found in list two, I use the copy , paste method to log it in my compare sheet. The code then goes back and does the reverse procedure to test list two on list one; loop through list two and look for each item in list 1.
My Results imply and inconsistent number of matching results. The number of items in list 1 minus the results from the "list1" find loop do not equal number of items in list two minus the number of items from "list2' find loop. The remaining items should only be values that where found in each list.
All items are primary keys and are unique within their respective list.
Public Sub compare_list()
Dim wsList2 As Worksheet
Dim wsList1 As Worksheet
Dim wsCompare As Worksheet
Dim found1 As Range
Set wsList2 = Worksheets("List2")
Set wsList1 = Worksheets("List1")
Set wsCompare = Worksheets("Compare")
Application.ScreenUpdating = False
'Check each value in client_id list one with list two
wsList1.Activate
wsList1.Range("a1").Select
Do Until ActiveCell.Value = ""
Set found1 = wsList2.Range("a1",_
wsList2.Range("A1048576").End(xlUp)).Find(Selection.Value)
If found1 Is Nothing Then
Selection.Copy
wsCompare.Range("A1048576").End(xlUp).Offset(1, 0).PasteSpecial
End If
ActiveCell.Offset(1, 0).Select
Loop
'Check each value in client_id list two with list one
wsList2.Activate
wsList2.Range("a1").Select
Do Until ActiveCell.Value = ""
Set found1 = wsList1.Range("a1",_
wsList1.Range("A1048576").End(xlUp)).Find(Selection.Value)
If found1 Is Nothing Then
Selection.Copy
wsCompare.Range("B1048576").End(xlUp).Offset(1, 0).PasteSpecial
End If
ActiveCell.Offset(1, 0).Select
Loop
wsCompare.Activate
Application.ScreenUpdating = True
End Sub
Just a note: I am very new to VBA, and do not come from a developer back ground. You probably can't dumb down your solution too much.

My problem was a default parameter in the find function. I needed to set the LookAt:=xlWhole
For example, list 1 = (1,2,32,142) and list 2 = (1,2,3,132)
For my purposes 32 is not in list 2 but without setting that parameter the find function returns a result when search for 32 as it finds it in the cell containing 132
The following is the code that is working for me now. Thanks for all your help
Public Sub compare_list()
Dim wsList2 As Worksheet
Dim wsList1 As Worksheet
Dim wsCompare As Worksheet
Dim found1 As Range
Dim found2 As Range
Dim myCell As Range
Dim countList2 As Integer
Dim countList1 As Integer
Dim listDiff As Integer
Dim commonList2 As Integer
Dim commonList1 As Integer
Dim diffList1 As Integer
Dim diffList2 As Integer
Set wsList2 = Worksheets("List2")
Set wsList1 = Worksheets("List1")
Set wsCompare = Worksheets("Compare")
Application.ScreenUpdating = False
Application.CutCopyMode = False
'Check each value in the client_id list created by List1 to find an equal value in List2's list
Set myCell = wsList1.Range("A1")
Do Until myCell.Value = ""
Set found1 = wsList2.Range("a1", wsList2.Range("A1048576").End(xlUp)).Find(what:=myCell.Value, LookAt:=xlWhole)
If found1 Is Nothing Then
myCell.Copy
wsCompare.Range("A1048576").End(xlUp).Offset(1, 0).PasteSpecial
Else
myCell.Copy
wsCompare.Range("G1048576").End(xlUp).Offset(1, 0).PasteSpecial
End If
Set myCell = myCell.Offset(1, 0)
Loop
'Check each value in the client_id list created by List2 to find an equesl value in List1's list
Set myCell = wsList2.Range("A1")
Do Until myCell.Value = ""
Set found2 = wsList1.Range("a1", wsList1.Range("A1048576").End(xlUp)).Find(what:=myCell.Value, LookAt:=xlWhole)
If found2 Is Nothing Then
myCell.Copy
wsCompare.Range("B1048576").End(xlUp).Offset(1, 0).PasteSpecial
Else
myCell.Copy
wsCompare.Range("F1048576").End(xlUp).Offset(1, 0).PasteSpecial
End If
Set myCell = myCell.Offset(1, 0)
Loop
Application.ScreenUpdating = True
wsCompare.Activate
'test logic of result
countList1 = wsList1.Range("a2", wsList1.Range("A1048576").End(xlUp)).Rows.count
countList2 = wsList2.Range("a2", wsList2.Range("a1048576").End(xlUp)).Rows.count
diffList1 = (wsCompare.Range("a2", wsCompare.Range("A1048576").End(xlUp)).Rows.count - 1)
diffList2 = (wsCompare.Range("b2", wsCompare.Range("b1048576").End(xlUp)).Rows.count - 1)
listDiff = Abs(countList1 - countList2)
commonList2 = (countList2 - diffList2)
commonList1 = (countList1 - diffList1)
MsgBox "List2 has " & commonList2 & " in common with List1" & vbCrLf & "List1 has " & commonList1 & " in common with List2"
End Sub

Related

VBA: adding up irregular ranges

I need some help to create a macro which adds all the values on the column E between the rows with the "avg" word. the result should be displayed on the cells where the "Sum here" label is displayed. Both texts "avg" and "sum here" is just for illustrate the example, "avg" could be replaced by any other word and "sum here" should actually be the aggregation of the values above it.
The real challenge is that the number of ranges on column E is variable, so i would like to find a macro which is able to deal with "n" number of ranges on column E.
Finally, the values on column D are only the example of the expected value on the "sum here" cells.
This is what I have tried to far:
Sub Macro1()
'
' Macro1 Macro
'
Dim sumhere As Range
Dim startingpoint As Range
Dim endingpoint As Range
'
Range("C17").Select
Selection.End(xlDown).Select
If ActiveCell = "avg" Then
ActiveCell.Offset(rowoffset:=0, columnoffset:=2).Select
Set sumhere = ActiveCell
Set startingpoint = ActiveCell.Offset(rowoffset:=-1, columnoffset:=0)
Selection.End(xlUp).Select
If (ActiveCell.Value) = "Sum here" Then
Set endingpoint = ActiveCell.Offset(rowoffset:=1, columnoffset:=0)
sumhere.Formula = "=sum(range(startingpoint:endingpoint)"
Else
Selection.End(xlUp).Select
If (ActiveCell.Value) = "Sum here" Then
Set endingpoint = ActiveCell.Offset(rowoffset:=1, columnoffset:=0)
sumhere.Formula = "=Sum(Range(startingpoint.adress:endingpoint.adress))"
Else: End If
End If
End If
End Sub
Additionally, as you can see, I do not know, how to define a range using variables. My original idea was to combine this code with some kind of "do while" or/and "for i= 1 to x" and "next i". But I can't see how to combine it.
Using formula only, and providing that column A only has avg (or any text) on each subtotal row.
I've given two versions of the formula - the volatile version (updates everytime you change anything on the spreadsheet), and the non-volatile version (only updates if it needs to).
The formula should be entered on row 6 - change the $E6 to which ever row you need.
(volatile)
=SUM(OFFSET($E6,IFERROR(LOOKUP(2,1/($A$1:INDEX($A:$A,ROW()-1)<>""),ROW($A$1:INDEX($A:$A,ROW()-1))),0)-ROW()+1,,ROW()-1-IFERROR(LOOKUP(2,1/($A$1:INDEX($A:$A,ROW()-1)<>""),ROW($A$1:INDEX($A:$A,ROW()-1))),0)))
(non volatile):
=SUM(INDEX($E:$E,IFERROR(LOOKUP(2,1/($A$1:INDEX($A:$A,ROW()-1)<>""),ROW($A$1:INDEX($A:$A,ROW()-1))),0)+1):INDEX($E:$E,ROW()-1))
or if you don't mind using a helper column:
In cell B6:
=IFERROR(LOOKUP(2,1/($A$1:INDEX($A:$A,ROW()-1)<>""),ROW($A$1:INDEX($A:$A,ROW()-1))),0)
In E6: (volatile)
=SUM(OFFSET($E6,$B6-ROW()+1,,ROW()-1-$B6))
or (non volatile):
=SUM(INDEX($E:$E,$B6):INDEX($E:$E,ROW()-1))
Edit:
Thought I'd add a UDF to calculate it to if you're after VBA.
Use the function =AddSubTotal() in the rows you want the sub total to be shown in, or use =AddSubTotal("pop",6) to sum everything in column F (col 6) using "pop" rather than "avg".
Public Function AddSubTotal(Optional Delim As String = "avg", Optional ColNumber = 5) As Double
Dim rCaller As Range
Dim rPrevious As Range
Dim rSumRange As Range
Set rCaller = Application.Caller
With rCaller.Parent
Set rPrevious = .Range(.Cells(1, 1), .Cells(rCaller.Row - 1, 1)).Find(Delim, , , , , xlPrevious)
If Not rPrevious Is Nothing Then
Set rSumRange = rPrevious.Offset(1, ColNumber - 1).Resize(rCaller.Row - rPrevious.Row - 1)
Else
Set rSumRange = .Range(.Cells(1, ColNumber), .Cells(rCaller.Row - 1, ColNumber))
End If
End With
AddSubTotal = WorksheetFunction.Sum(rSumRange)
End Function
The following VBA routine assumes that
your data is in Columns C:E
Nothing else relevant (nothing numeric) in that range
Your "key word" where you want to show the sum is avg
avg (the key word) is hard-coded in the macro
You could easily modify this routine to also perform an average of those values, and put those results, for example, in Column D
Any of the above are easily modified
Option Explicit
Sub TotalSubRanges()
Dim vSrc As Variant, rSrc As Range
Dim dAdd As Double
Dim I As Long
Const sKey As String = "avg"
Set rSrc = Range(Cells(1, "C"), Cells(Rows.Count, "C").End(xlUp)).Resize(columnsize:=3)
vSrc = rSrc
'Do the "work" in a VBA array, as this will
' execute much faster than working directly
' on the worksheet
For I = 1 To UBound(vSrc, 1)
If vSrc(I, 1) = sKey Then
vSrc(I, 3) = dAdd
dAdd = 0
Else
If IsNumeric(vSrc(I, 3)) Then dAdd = dAdd + vSrc(I, 3)
End If
Next I
'write the results back to the worksheet
' and conditionally format the "sum" cells
With rSrc
.EntireColumn.Clear
.Value = vSrc
.Columns(3).AutoFit
.EntireColumn.ColumnWidth = .Columns(3).ColumnWidth
.FormatConditions.Delete
.FormatConditions.Add _
Type:=xlExpression, _
Formula1:="=" & .Item(1, 1).Address(False, True) & "=""" & sKey & """"
With .FormatConditions(1)
.Interior.ColorIndex = 6
End With
End With
End Sub
Surely you just need something like:
Sub sums()
Dim i As Integer, j As Integer, k As Integer
j = Range("C1048576").End(xlUp).Row
k = 1
For i = 1 To j
If Range("C" & i).Value <> "" Then
Range("E" & i).Value = "=Sum(E" & k & ":E" & i - 1 & ")"
k = i + 1
End If
Next i
End Sub
Change:
Dim startingpoint As Range
Dim endingpoint As Range
To:
Dim startingpoint As Variant
Dim endingpoint As Variant
As the startingpoint and endingpoint is used in a formula, you cant define them as a Range.

Excel Macro to copy all cells containing current array value and paste them to a new workbook

I've been given a spreadsheet that contains a list of furniture suppliers in Column C. The other columns contain information about the different furniture products they stock. My task is to copy all the cells containing information about those furniture products for each supplier and paste them into a new workbook. There are around 66 different suppliers so obviously I don't really fancy doing this manually. I thought someone here would have had to have done a similar task and might know how I could go about writing a Macro to solve the problem.
So far I've managed to write the following code. It basically takes the users selection, loops through all the Cells in the selection. Takes the unique values (each new furniture supplier) and adds them to an array containing only the unique values. The problem I'm having is working out what to do next.
Sub addItemsToArray()
Dim varIn As Variant 'User Selection
Dim varUnique As Variant 'Array containing Unique Values
Dim iInRow As Long 'Variable storing current row number
Dim iUnique As Long 'Variable storing current unqiue array value
Dim nUnique As Long 'Variable storing number of unique values in User Selection.
Dim isUnique As Boolean 'Boolean Variable indicating whether current value is unique or not
Dim sValue As Long 'I have included these two values to find start and end position for unique values in user Selection
Dim lValue As Long
varIn = Selection
ReDim varUnique(1 To UBound(varIn, 1) * UBound(varIn, 2)) 'Set upper and lower bounds for VarUnique array, lower bound will be 1, upper will be last cell in selection
nUnique = 0 'Number of Unique values set as 0 by default
'Looping through all Values in User Selection
For iInRow = LBound(varIn, 1) To UBound(varIn, 1)
isUnique = True 'First value will always be unique so set isUnique to True
'Loop through from 1 to the Number of Unique Values in Array. Set to 0 by default.
'If CurrentCell Value is equal to element in array then it is not Unique, as such isUnique will be set to False and For loop will be exited.
For iUnique = 1 To nUnique
If varIn(iInRow, 1) = varUnique(iUnique) Then
isUnique = False
Exit For
End If
Next iUnique
If isUnique = True Then
sValue = lValue
nUnique = nUnique + 1
varUnique(nUnique) = varIn(iInRow, 1)
lValue = iInRow
End If
Next iInRow
'// varUnique now contains only the unique values.
'// Trim off the empty elements:
ReDim Preserve varUnique(1 To nUnique)
End Sub
If anyone could help point me in the right direction I would very much appreciate it.
I've included an image of the worksheet below. As you can see Column C contains the list of suppliers. What I need to do is, copy all cells for each supplier, place those cells in a new worksheet and save it, with the name of the supplier as the file name. I hope that's made it a bit clearer.
Sub Parse_Furniture_Suppliers()
Dim tmpCell As Range, rngHeaders As Range, rngTarget As Range
Set rngHeaders = ActiveSheet.Range("A1:F1")
Set tmpCell = ActiveSheet.Range("C2")
Workbooks.Add
ActiveSheet.Range("A1:F1").Value = rngHeaders.Value
Set rngTarget = ActiveSheet.Range("A2")
rngTarget.Select
ActiveWindow.FreezePanes = True
rngTarget.Resize(1, 6).Value = tmpCell.Offset(0, -2).Resize(1, 6).Value
Set rngTarget = rngTarget.Offset(1)
Set tmpCell = tmpCell.Offset(1)
Do While tmpCell.Value <> ""
If tmpCell.Value <> tmpCell.Offset(-1).Value Then
ActiveWorkbook.SaveAs tmpCell.Offset(-1).Value
ActiveWorkbook.Close
Workbooks.Add
ActiveSheet.Range("A1:F1").Value = rngHeaders.Value
Set rngTarget = ActiveSheet.Range("A2")
rngTarget.Select
ActiveWindow.FreezePanes = True
End If
rngTarget.Resize(1, 6).Value = tmpCell.Offset(0, -2).Resize(1, 6).Value
Set rngTarget = rngTarget.Offset(1)
Set tmpCell = tmpCell.Offset(1)
Loop
ActiveWorkbook.SaveAs tmpCell.Offset(-1).Value
ActiveWorkbook.Close
End Sub

Collect unique identifiers from one column and paste the results in a different worksheet.

What I'm looking to do is comb through a column and pull all the unique identifiers out of that column and then paste the results in a table in a different worksheet. I found the code below and it is very close to what I need. However, I have two major problems with it that I cannot figure out. First the area that this macro searches is constant ie "A1:B50". I need this to be one column and be dynamic since more data and new unique identifiers will be added to this worksheet. Second I cannot figure out how to paste my results to a specific range on a different worksheet. For example if I wanted to take the results and paste them in "sheet2" starting in at "B5" and going to however long the list of unique identifiers is.
Sub ExtractUniqueEntries()
Const ProductSheetName = "Sheet1" ' change as appropriate
Const ProductRange = "B2:B"
Const ResultsCol = "E"
Dim productWS As Worksheet
Dim uniqueList() As String
Dim productsList As Range
Dim anyProduct
Dim LC As Integer
ReDim uniqueList(1 To 1)
Set productWS = Worksheets(ProductSheetName)
Set productsList = productWS.Range(ProductRange)
Application.ScreenUpdating = False
For Each anyProduct In productsList
If Not IsEmpty(anyProduct) Then
If Trim(anyProduct) <> "" Then
For LC = LBound(uniqueList) To UBound(uniqueList)
If Trim(anyProduct) = uniqueList(LC) Then
Exit For ' found match, exit
End If
Next
If LC > UBound(uniqueList) Then
'new item, add it
uniqueList(UBound(uniqueList)) = Trim(anyProduct)
'make room for another
ReDim Preserve uniqueList(1 To UBound(uniqueList) + 1)
End If
End If
End If
Next ' end anyProduct loop
If UBound(uniqueList) > 1 Then
'remove empty element
ReDim Preserve uniqueList(1 To UBound(uniqueList) - 1)
End If
'clear out any previous entries in results column
If productWS.Range(ResultsCol & Rows.Count).End(xlUp).Row > 1 Then
productWS.Range(ResultsCol & 2 & ":" & _
productWS.Range(ResultsCol & Rows.Count).Address).ClearContents
End If
'list the unique items found
For LC = LBound(uniqueList) To UBound(uniqueList)
productWS.Range(ResultsCol & Rows.Count).End(xlUp).Offset(1, 0) = _
uniqueList(LC)
Next
'housekeeping cleanup
Set productsList = Nothing
Set productWS = Nothing
End Sub
I think your solution is a bit more tricky than it needs to be. Collecting unique ids becomes almost trivial is you use a Dictionary instead of a list. The added benefit is that a dictionary will scale much better than a list as your data set becomes larger.
The code below should provide you with a good starting point to get you going. For convenience's sake I used the reference from your post. So output will be on sheet2 to starting in cell B5 going down and the input is assumed to be on sheet1 cell B2 going down.
If you have any questions, please let me know.
Option Explicit
Sub ExtractUniqueEntries()
'enable microsoft scripting runtime --> tools - references
Dim unique_ids As New Dictionary
Dim cursor As Range: Set cursor = ThisWorkbook.Sheets("Sheet1").Range("B2") 'change as Required
'collect the unique ids
'This assumes that:
'1. ids do not contain blank rows.
'2. ids are properly formatted. Should this not be the could you'll need to do some validating.
While Not IsEmpty(cursor)
unique_ids(cursor.Value) = ""
Set cursor = cursor.Offset(RowOffset:=1)
Wend
'output the ids to some target.
'assumes the output area is blank.
Dim target As Range: Set target = ThisWorkbook.Sheets("Sheet2").Range("B5")
Dim id_ As Variant
For Each id_ In unique_ids
target = id_
Set target = target.Offset(RowOffset:=1)
Next id_
End Sub
A small modification will do it; the key is to define the ProductRange.
Sub ExtractUniqueEntries()
Const ProductSheetName = "Sheet1" ' change as appropriate
Dim ProductRange
ProductRange = "B2:B" & Range("B" & Cells.Rows.Count).End(xlUp).Row
Const ResultsCol = "E"
Dim productWS As Worksheet
Dim uniqueList() As String
Dim productsList As Range
Dim anyProduct
Dim LC As Integer
ReDim uniqueList(1 To 1)
Set productWS = Worksheets(ProductSheetName)
Set productsList = productWS.Range(ProductRange)
Application.ScreenUpdating = False
For Each anyProduct In productsList
If Not IsEmpty(anyProduct) Then
If Trim(anyProduct) <> "" Then
For LC = LBound(uniqueList) To UBound(uniqueList)
If Trim(anyProduct) = uniqueList(LC) Then
Exit For ' found match, exit
End If
Next
If LC > UBound(uniqueList) Then
'new item, add it
uniqueList(UBound(uniqueList)) = Trim(anyProduct)
'make room for another
ReDim Preserve uniqueList(1 To UBound(uniqueList) + 1)
End If
End If
End If
Next ' end anyProduct loop
If UBound(uniqueList) > 1 Then
'remove empty element
ReDim Preserve uniqueList(1 To UBound(uniqueList) - 1)
End If
'clear out any previous entries in results column
If productWS.Range(ResultsCol & Rows.Count).End(xlUp).Row > 1 Then
productWS.Range(ResultsCol & 2 & ":" & _
productWS.Range(ResultsCol & Rows.Count).Address).ClearContents
End If
'list the unique items found
For LC = LBound(uniqueList) To UBound(uniqueList)
productWS.Range(ResultsCol & Rows.Count).End(xlUp).Offset(1, 0) = _
uniqueList(LC)
Next
'housekeeping cleanup
Set productsList = Nothing
Set productWS = Nothing
End Sub

Copy/Paste Specific Columns from a Worksheet to another

I want to copy some columns with headers from a worksheet to another one. I've created an array that looks for the different headers needed so I can copy and paste the entire column into the new tab. I know I have an error somewhere because I'm getting a type mismatch error and possibly other types as well. Can someone take a look and see what I'm missing/have wrong?
Dim rngCell As Range
Dim strHeader() As String
Dim intColumnsMax As Integer
Sheets.Add.Name = "Material Master"
Sheets.Add.Name = "BOM"
intColumnsMax = Sheets("HW Zpure Template").UsedRange.Columns.Count
ReDim strHeader(1 To intColumnsMax)
strHeader(1) = "MATERIAL"
strHeader(2) = "MATERIAL TYPE"
strHeader(3) = "MATERIAL DESCRIPTION"
For Each rngCell In Rows(4)
For i = 1 To intColumnsMax
If strHeader(i) = rngCell.Value Then
rngCell.EntireColumn.Copy
Sheets("Material Master").Select
ActiveSheet.Paste Destination:=Worksheets("Material Master").Cells(1, i)
Sheets("HW Zpure Template").Select
End If
Next i
Next
I prefer to use Application.Match to locate a specific column header label rather than cycling through them trying to find a match. To that end, I've heavily modified your code.
Dim c As Long, v As Long, vHDRs As Variant
Dim s As Long, vNWSs As Variant, wsMM As Worksheet
vHDRs = Array("MATERIAL", "MATERIAL TYPE", "MATERIAL DESCRIPTION")
vNWSs = Array("Material Master", "BOM")
For v = LBound(vNWSs) To UBound(vNWSs)
For s = 1 To Sheets.Count
If Sheets(s).Name = vNWSs(v) Then
Application.DisplayAlerts = False
Sheets(s).Delete
Application.DisplayAlerts = True
Exit For
End If
Next s
Sheets.Add after:=Sheets(Sheets.Count)
Sheets(Sheets.Count).Name = vNWSs(v)
Next v
Set wsMM = Sheets("Material Master")
With Sheets("HW Zpure Template")
For v = LBound(vHDRs) To UBound(vHDRs)
If CBool(Application.CountIf(.Rows(4), vHDRs(v))) Then
c = Application.Match(vHDRs(v), .Rows(4), 0)
Intersect(.UsedRange, .Columns(c)).Copy _
Destination:=wsMM.Cells(1, Application.CountA(wsMM.Rows(1)) + 1)
End If
Next v
End With
Set wsMM = Nothing
Correct me if I'm wrong, but your code seemed to be looking for the column labels in row 4. That is what I'm using above but if that assumption is incorrect then the fix should be fairly self-evident. I've also stacked the copied columns into the first available column to the right. Your code may have been putting them in the original position.
When you run the above, please note that it will remove worksheets named Material Master or BOM without asking in favor of inserting its own worksheets of those names. Given that, it's probably best to run on a copy of your original.
Using the Find() method is a very efficient way of finding the data you want. Below are a few suggestions to optimize your existing code.
Dim rngCell As Range
Dim strHeader() As String
Dim intColumnsMax As Integer
Dim i As Integer
Sheets.Add.Name = "Material Master"
Sheets.Add.Name = "BOM"
'Quick way to load a string array
'This example splits a comma delimited string.
'If your headers contain commas, replace the commas in the next line of code
'with a character that does not exist in the headers.
strHeader = Split("MATERIAL,MATERIAL TYPE,MATERIAL DESCRIPTION", ",")
'Only loop through the headers needed
For i = LBound(strHeader) To UBound(strHeader)
Set rngCell = Sheets("HW Zpure Template").UsedRange.Find(What:=strheader(i), LookAt:=xlWhole)
If Not rngCell Is Nothing Then
'Taking the intersection of the used range and the entire desired column avoids
'copying a lot of unnecessary cells.
Set rngCell = Intersect(Sheets("HW Zpure Template").UsedRange, rngCell.EntireColumn)
'This method is more memory consuming, but necessary if you need to copy all formatting
rngCell.Copy Destination:=Worksheets("Material Master").Range(rngCell.Address)
'This method is the most efficient if you only need to copy the values
Worksheets("Material Master").Range(rngCell.Address).Value = rngCell.Value
End If
Next i

Excel VBA validation errors when a spreadsheet is proteced

I'm trying to ensure that data entered into the named range of an Excel spreadsheet is valid. To do this, I've defined a static validation list for column "A" in the range, and enabled the dropdown list for that column. Based on the option selected by the user, I add a validation object in column "B" at runtime, having a list of entries constrained by the entry in column "A". Based on the entries in columns A and B, the cell in column "C" is automatically populated.
This works fine until spreadsheet protection is enabled. At that point, attempting to select an option from the droplist in column "B" generates the following error:
"The cell or chart that you are trying to change is protected and therefore read-only. ... "
However
All cells in the range in question were unlocked prior to adding
worksheet protection.
The code explicitly removes protection prior to updating the
validation object in column "B", then replaces it once the validation
object has been added.
When a list item is selected from the droplist in column "B", the
error message fires immediately before any worksheet events occur,
making it impossible to trap or debug the error.
I have code in both the spreadsheet and in a separate code module, both or which are included below. Any ideas would be greatly appreciated
Here's the code in the Worksheet_Change() event:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim strNm As String
' there will be multiple named ranges eventually. We need to be able to distinguish
' among the various ranges so that our code executes only against the data we expect
' to manipulate - not random cells
If Not Intersect(ActiveCell, ActiveWorkbook.Names("DBAddRange").RefersToRange) Is Nothing Then
Dim rng As Range
Set rng = ActiveWorkbook.Names("DBAddRange").RefersToRange
If Target.Column = 1 Then
If FLAG_CHANGE_IN_PROGRESS = True Then Exit Sub
FLAG_CHANGE_IN_PROGRESS = True
Dim VldnList As String
VldnList = getVldtnList(Target.Value)
unlockSS ActiveSheet
Range("B" & Target.row).Clear
Range("B" & Target.row).Select
With Range("B" & Target.row).Validation
.Delete
.Add Type:=xlValidateList, Operator:=xlValidateList, Formula1:=VldnList
.IgnoreBlank = False
.InCellDropdown = True
End With
lockSS ActiveSheet
Range("B" & Target.row).Select
FLAG_CHANGE_IN_PROGRESS = False
ElseIf Target.Column = 2 Then
If FLAG_CHANGE_IN_PROGRESS = True Then Exit Sub
FLAG_CHANGE_IN_PROGRESS = True
unlockSS ActiveSheet
Dim dbHost As Variant
Dim hNmRng As Range
Set hNmRng = ActiveWorkbook.Names("valid_lookups").RefersToRange
dbHost = Application.VLookup(Target.Value, hNmRng, 2, False)
Range("C" & Target.row).Value = dbHost
lockSS ActiveSheet
FLAG_CHANGE_IN_PROGRESS = False
End If
End If
If Not Intersect(ActiveCell, ActiveWorkbook.Names("HostAddRange").RefersToRange) Is Nothing Then
End If
End Sub
Code in the external module:
Sub lockSS(ByVal sheet As Sheet1)
sheet.Protect Password:=[NOT SHOWN], UserInterfaceOnly:=True, DrawingObjects:=False
Application.EnableEvents = True
End Sub
Function getVldtnList(ByVal dbName As String)
Dim vrtmatchRow As Variant
Dim rng As Range
If dbName = "" Then
getVldtnList = ""
Exit Function
End If
' this is a pre-defined range having entries for:
' DB Name - Column 1
' DB CI ID - Column 2
' DB Host - Column 3
Set rng = ActiveWorkbook.Names("valid_db_nms").RefersToRange
' find the value of the first row in the range that matches the value
' of the dbName parm. NOTE: the final 0 parm tells the match function
' to find an exact match.
vrtmatchRow = Application.Match(dbName, rng, 0)
If IsError(vrtmatchRow) Then
' NOTE: we should NEVER get here due to the way cell validation is set up.
MsgBox "The value entered was not found in the list of valid database values. See xxx for help", vbRetryCancel, "Invalid Entry"
Else
Dim row As Long
Dim strListVals As String
Set rng = ActiveWorkbook.Names("valid_db_info").RefersToRange
row = vrtmatchRow
Do
If Len(strListVals) > 0 Then strListVals = strListVals + ","
strListVals = strListVals + rng.Cells(row, 2).Value
row = row + 1
Loop While (rng.Cells(row, 1).Value = dbName)
End If
getVldtnList = strListVals
End Function
Sub unlockSS(ByVal sheet As Sheet1)
sheet.Unprotect Password:=[NOT SHOWN]
Application.EnableEvents = False
End Sub
Clearing a range will also reset the "locked" checkbox, so you need to reset that each time
Range("B" & Target.row).Clear