VBA sub not entering functions - excel

I created the below functions in different sheets in my Excel workbook and they are all executed by a sub. However when I start the sub it doesn't access the functions and directly skips the If statements.
Sheet1 functions:
Public Function ndg(row As Range) As Integer
ndg = Range("b" & row).Value
End Function
Function Checkispraticaforeclosure(row As Range)
Dim ndga As Integer
Dim rowindex As Integer
ndga = ndg(row)
rowindex = Sheet6.findrownumberndg(ndga)
If Sheet6.ispositionforeclosure(rowindex) = True Then
row.Cells(33) = Sheet6.getforeclosurecode(rowindex)
Checkispraticaforeclosure = True
Else
Checkispraticaforeclosure = False
End If
End Function
Sub insertcode(row As Range)
Dim ndga As Integer
Dim rowindex As Integer
ndga = ndg(row)
rowindex = Sheet6.findrownumberndg(ndga)
If Sheet6.ispositionforeclosure(rowindex) = False Then
row.Cells(33) = "ok"
End If
End Sub
Sheet6 functions:
Public Function findrownumberndg(ndg As Integer) As Integer
Set foundcell = Sheet6.Range("a:a").Find(ndg, lookat:=xlWhole)
If Not foundcell Is Nothing Then
findrownumberndg = 0
Else
findrownumberndg = foundcell.row
End If
End Function
Public Function ispositionforeclosure(row As Integer) As Boolean
If Range("D" & row).Value = "Foreclosure procedure" Then
ispositionforeclosure = True
Else
ispositionforeclosure = False
End If
End Function
Public Function getforeclosurecode(row As Integer)
getforeclosurecode = Range("f" & row).Value
End Function
Execute sub:
Public Function sheet1lastrow()
lastrow = Sheet1.Cells(Rows.Count, "b").End(xlUp).row
End Function
Sub masterinsertcode()
For x = 6 To ThisWorkbook.sheet1lastrow()
Dim checked
checked = False
Dim currentrow As Range
Set currentrow = Sheet1.Rows(x)
checked = Sheet1.Checkispraticaforeclosure(currentrow)
If checked = False Then
Sheet1.insertcode (currentrow)
End If
Next
End Sub
I am unsure why the execute sub doesn't enter in the checks I created and directly goes to End Sub.

Related

Searchable combobox not working with collection

I have a dropdown that before was being populated with values from a second sheet in my workbook using the following code:
Private Sub UserForm_Initialize()
Dim cProd As Range
Dim ws As Worksheet
Dim i As Long
Set ws = ThisWorkbook.Worksheets("DO NOT DELETE")
For Each cProd In ws.Range("ProdList")
With Me.dropProd
.AddItem cProd.Value
End With
Next cProd
Me.dropProd.SetFocus
End Sub
Then, I added the code I found here to add the searchable functionality to it, and it was working just fine.
Then I had to tweak my code to add a second dropdown that would be dependent on the first one that I had previously. To do that, I deleted that DO NOT DELETE worksheet, and created two collections to store the values for the dropdowns.
Now, my first dropdown is being populated in this code:
Sub UpdateAll()
Dim ProdID As String
Dim Prod As String
Dim TF As Boolean
Dim lRow As Long
Dim i, t, s
dropProd.Clear
dropPromo.Clear
Set ws = ThisWorkbook.Worksheets("Table View")
Set cProd = New Collection
lRow = ws.Cells(Rows.Count, 1).End(-4162).Row
For i = 13 To lRow
ProdID = ws.Cells(i, 2).Value
Prod = ws.Cells(i, 3).Value
If ProdID <> "" Then
TF = False
If cProd.Count <> 0 Then
For t = 1 To cProd.Count
If cProd(t) = ProdID & " - " & Prod Then TF = True
Next
End If
If TF = False Then cProd.Add (ProdID & " - " & Prod)
End If
Next
For s = 1 To cProd.Count
dropProd.AddItem (cProd(s))
Next
End Sub
Private Sub UserForm_Initialize()
Me.dropProd.SetFocus
UpdateAll
End Sub
This part is also doing great, the below is where I'm having trouble with:
Private Sub dropProd_Change()
Dim ProdInfo As String
Dim Promo As String
Dim q, p
dropPromo.Clear
lRow = ws.Cells(Rows.Count, 1).End(-4162).Row
If dropProd.Value <> "" Then
ProdInfo = Mid(dropProd.Value, 1, InStr(1, dropProd.Value, " - ") - 1)
End If
'Populates Promo ComboBox
For q = 13 To lRow
Promo = ws.Cells(q, 9).Value
If ws.Cells(q, 2).Value = ProdInfo Then dropPromo.AddItem Promo
Next
End Sub
The above works fine if I just select the value from the dropdown, but it breaks every time I try to search anything, and the problem is in this line ProdInfo = Mid(dropProd.Value, 1, InStr(1, dropProd.Value, " - ") - 1)
I've tried to rewrite it in another way, but it's still throwing me an error. Also, I tried to incorporate the code from the link above to see if it would work, but then I didn't know what to reference on me.dropProd.List = ????. I've tried haing this equals to the Collection I have, and of course it didn't work, and now I'm stuck on how to fix it.
I couldn't reproduce the problem with your code line ProdInfo = Mid(dropProd.Value, 1, InStr(1, dropProd.Value, " - ") - 1), it might be data related. Try this alternative ProdInfo = Trim(Split(dropProd.Value, "-")(0)) and a dictionary rather than a collection.
Option Explicit
Dim ws
Sub UpdateAll()
Dim ProdID As String, Prod As String
Dim lastrow As Long, i As Long
dropProd.Clear
dropPromo.Clear
Dim dictProd As Object, k As String
Set dictProd = CreateObject("Scripting.DIctionary")
Set ws = ThisWorkbook.Worksheets("Table View")
With ws
lastrow = ws.Cells(.Rows.Count, 1).End(xlUp).Row
For i = 13 To lastrow
ProdID = Trim(.Cells(i, 2))
If Len(ProdID) > 0 Then
Prod = Trim(.Cells(i, 3))
k = ProdID & " - " & Prod
If Not dictProd.exists(k) Then
dictProd.Add k, 1
End If
End If
Next
dropProd.List = dictProd.keys
End With
End Sub
Private Sub dropProd_Change()
Dim ProdInfo As String, Promo As String
Dim lastrow As Long, i As Long
dropPromo.Clear
If dropProd.Value <> "" Then
ProdInfo = Trim(Split(dropProd.Value, "-")(0))
'Populates Promo ComboBox
With ws
lastrow = ws.Cells(.Rows.Count, 1).End(xlUp).Row
For i = 13 To lastrow
If .Cells(i, 2).Value = ProdInfo Then
Promo = ws.Cells(i, 9).Value
dropPromo.AddItem Promo
End If
Next
End With
End If
End Sub
Private Sub UserForm_Initialize()
Me.dropProd.SetFocus
UpdateAll
End Sub

How to use Range.ClearContents() within a Application.Evaluate() context in Excel2010

The ClearContents() method appears to be broken inside an Evaluate() context
minimal testing example
Dim ForceRunOnceTogg As Boolean
Public Sub cc_test()
Dim s As Range: Set s = Selection.Cells(1)
ForceRunOnceTogg = False
Evaluate Replace("cc(%1)", "%1", s.Address)
End Sub
Private Function cc(c As Range)
If ForceRunOnceTogg Then Exit Function
ForceRunOnceTogg = True 'also not sure why eval double fires
Debug.Print "marco" 'show double vs single eval fire
c.Value = "bananas" 'show it has correct cell
c.Interior.ColorIndex = 6 'Yellow
c.AddComment "a comment" 'show can run methods on cell
c.ClearContents ' this specific method doesn't fire?
End Function
I have tried various options to get ClearContents to fire inside the Evaluate, including just passing the address as a string and using set c = Application.Range(addr); however I have yet to find an option where ClearContents is working. Is there maybe a way to workaround ClearContents and still leave the cell in the same state as if it were cleared?
Otherwise, I guess I'm looking at a replacement for Evaluate in my usage case
minimal usage example
Dim ForceRunOnceTogg As Boolean
Private Sub MapSelection(Lambda As String)
Dim r As Range, ar As Range, col As Range
Set r = Selection
Dim i As Long, rowCount As Long, FirstRow As Long, func As String
Dim scrn As Boolean: scrn = Application.ScreenUpdating: Application.ScreenUpdating = False
Dim calc As XlCalculation: calc = Application.Calculation: Application.Calculation = xlCalculationManual
On Error GoTo RestApp
For Each ar In r.Areas
rowCount = ar.Columns(1).Cells.count
FirstRow = ar.Cells(1).Row - 1
For Each col In ar.Columns
For i = 1 To rowCount
If IsEmpty(col.Cells(i)) Then i = col.Cells(i).End(xlDown).Row - FirstRow
If i > rowCount Then Exit For
ForceRunOnceTogg = False
func = Replace(Lambda, "%1", col.Cells(i).Address(External:=False))
' Debug.Print func
Application.Evaluate func
Next i
Next col
Next ar
Application.Calculation = calc: Application.Calculate
Application.ScreenUpdating = scrn
On Error GoTo 0
Exit Sub
RestApp:
Application.Calculation = calc: Application.Calculate
Application.ScreenUpdating = scrn
On Error GoTo 0
Resume
End Sub
Private Function clearJunk_cell(c As Range)
If ForceRunOnceTogg Then Exit Function
ForceRunOnceTogg = True
If IsError(c.Value) Then
c.ClearContents ' ClearContents won't fire in this context
ElseIf c.Value = "" Then
c.ClearContents
ElseIf Strings.Trim(c.Value) = "" Then
c.ClearContents
End If
End Function
Private Function markJunk_cell(c As Range)
If ForceRunOnceTogg Then Exit Function
ForceRunOnceTogg = True
If IsError(c.Value) Then
c.Interior.Color = 16776960 'Bright Blue
ElseIf c.Value = "" Then
c.Interior.Color = 16776960
ElseIf Strings.Trim(c.Value) = "" Then
c.Interior.Color = 16776960
End If
End Function
Public Function ScrubText(text As String) As String
Dim i As Long, T As String, a As Long
For i = 1 To Len(text)
T = Mid(text, i, 1)
a = AscW(T)
If 31 < a And a < 128 Then ScrubText = ScrubText & T
Next i
End Function
Private Function Scrub_cell(c As Range)
If ForceRunOnceTogg Then Exit Function
ForceRunOnceTogg = True
c.Value2 = ScrubText(c.Value2)
End Function
Private Function markScrub_cell(c As Range)
If ForceRunOnceTogg Then Exit Function
ForceRunOnceTogg = True
If c.Value2 <> ScrubText(c.Value2) Then
c.Interior.Color = 16776960 'Bright Blue
End If
End Function
Public Sub clearJunk(): MapSelection "clearJunk_cell(%1)": End Sub
Public Sub markJunk(): MapSelection "markJunk_cell(%1)": End Sub
Public Sub scrubSelection(): MapSelection "Scrub_cell(%1)": End Sub
Public Sub markScrubSelection(): MapSelection "markScrub_cell(%1)": End Sub
Where clearJunk(), markJunk(), scrubSelection(), and markScrubSelection() along with other similar subs are called from ribbon buttons.
Thanks to the comments, it turns out that Application.Run() made for a much cleaner solution.
Enum SpeedSetting: Fastest: Fast: Medium: Slow: End Enum
Public Sub SetSpeedUp(Optional Speed As SpeedSetting = Slow)
Select Case Speed
Case Fastest
Application.ScreenUpdating = False
Application.EnableEvents = False
Application.DisplayStatusBar = False
Application.Calculation = xlCalculationManual
ActiveSheet.DisplayPageBreaks = False
Case Fast
Application.ScreenUpdating = False
Application.EnableEvents = True
Application.DisplayStatusBar = True
Application.Calculation = xlCalculationManual
ActiveSheet.DisplayPageBreaks = False
Case Medium
Application.ScreenUpdating = False
Application.EnableEvents = True
Application.DisplayStatusBar = True
Application.Calculation = xlCalculationAutomatic
ActiveSheet.DisplayPageBreaks = False
Case Else 'Slow
Application.ScreenUpdating = True
Application.EnableEvents = True
Application.DisplayStatusBar = True
Application.Calculation = xlCalculationAutomatic
ActiveSheet.DisplayPageBreaks = False
End Select
End Sub
Private Sub Map(CellMacro As String, ApplyToRange As Range, Optional Speed As SpeedSetting = Fastest)
Dim ar As Range, col As Range
Dim i As Long, rowCount As Long, FirstRow As Long, MaxRow As Long
SetSpeedUp Speed
On Error GoTo RestApp
For Each ar In ApplyToRange.Areas
rowCount = ar.Columns(1).Cells.Count
FirstRow = ar.Cells(1).Row - 1
MaxRow = ar.Cells(1).EntireColumn.Cells.count
For Each col In ar.Columns
For i = 1 To rowCount
If IsEmpty(col.Cells(i)) Then i = col.Cells(i).End(xlDown).Row - FirstRow
If i > rowCount Then
Exit For
ElseIf i = MaxRow Then
If IsEmpty(col.Cells(i)) Then Exit For
End If
Application.Run CellMacro, col.Cells(i)
Next i
Next col
Next ar
SetSpeedUp Slow
On Error GoTo 0
Exit Sub
RestApp:
SetSpeedUp Slow
On Error GoTo 0
Resume
End Sub
Public Sub clearJunk(): Map "clearJunk_cell", Selection: End Sub
Private Sub clearJunk_cell(c As Range)
If IsError(c.Value) Then
c.ClearContents
ElseIf c.Value = "" Then
c.ClearContents
ElseIf Strings.Trim(c.Value) = "" Then
c.ClearContents
End If
End Sub
Public Sub markJunk(): Map "markJunk_cell", Selection: End Sub
Private Sub markJunk_cell(c As Range)
If IsError(c.Value) Then
c.Interior.Color = 16776960 'Bright Blue
ElseIf c.Value = "" Then
c.Interior.Color = 16776960
ElseIf Strings.Trim(c.Value) = "" Then
c.Interior.Color = 16776960
End If
End Sub
Public Sub Touch(): Map "touch_cell", Selection: End Sub
Private Sub touch_cell(c As Range)
If Asc(c.Formula) <> 61 Then c.Value = c.Value
End Sub
Public Function ScrubText(text As String) As String
Dim i As Long, T As String, a As Long
For i = 1 To Len(text)
T = Mid(text, i, 1)
a = AscW(T)
If 31 < a And a < 128 Then ScrubText = ScrubText & T
Next i
End Function
Public Sub ScrubSelection(): Map "ScrubText_cell", Selection: End Sub
Private Sub ScrubText_cell(c As Range): c.Value2 = ScrubText(c.Value2): End Sub
Public Sub markScrubSelection(): Map "ScrubText_cell", Selection: End Sub
Private Function markScrub_cell(c As Range)
If c.Value2 <> ScrubText(c.Value2) Then
c.Interior.Color = 16776960 'Bright Blue
End If
End Function

VBA how to Create Multi Select ListBoxes in qualifying cells

I am trying to achieve code where multi-select ListBoxes are added if Column 4 or 5 are selected and Column 2 in the same row has the string "has options".
The Listboxes contain values from named ranges called "option1" and "option2". Current Selections are output to the respective cell in Column 4 or 5 separated by commas.
This is the code I have in "This Workbook" object. It needs to work on all sheets.
Private Sub Workbook_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range)
If Target.Cells.Count > 1 Then Exit Sub
If Target.Column = 4 And Target.OFFSET(0, -1).Value = "has options" Then
CreateOpt1PopUp Target
End If
If Target.Column = 5 And Target.OFFSET(0, -2).Value = "has options" Then
CreateOpt2PopUp Target
End If
Else
DeleteAllOpt1PopUps Target
DeleteAllOpt2PopUps Target
End If
End If
End Sub
This is the code I have in a Module. The criteria has evolved and therefore I have amended the code multiple times to the point where it no longer works.
Private opt1SelectCell As Range
Public Function Opt1Area(ByRef ws As Worksheet) As Range
Const OPT1_COL As Long = 4
Dim lastOpt1Row As Long
With ws
lastOpt1Row = .Cells(.Rows.Count, 1).End(xlUp).Row - 1
If lastOpt1Row = 0 Then
Set Opt1Area = Nothing
Else
Set Opt1Area = .Cells(2, OPT1_COL).Resize(lastOpt1Row, 1)
End If
End With
End Function
Public Sub Opt1BoxClick()
Dim opt1BoxName As String
opt1BoxName = Application.Caller
Dim opt1Box As ListBox
Set opt1Box = ActiveSheet.ListBoxes(opt1BoxName)
Dim opt1List As String
Dim i As Long
For i = 1 To opt1Box.ListCount
If opt1Box.Selected(i) Then
opt1List = opt1List & opt1Box.List(i) & ","
End If
Next i
If Len(opt1List) > 0 Then
opt1List = Left$(opt1List, Len(opt1List) - 1)
End If
opt1SelectCell.Value = opt1List
End Sub
Public Function Opt1ListArea() As Range
Set Opt1ListArea = ActiveSheet.Range("option1")
End Function
Public Sub DeleteAllOpt1PopUps(ByRef selectedCell As Range)
Dim opt1Box As ListBox
For Each opt1Box In selectedCell.Parent.ListBoxes
opt1Box.Delete
Next opt1Box
End Sub
Public Sub CreateOpt1PopUp(ByRef selectedCell As Range)
Set opt1SelectCell = selectedCell
Dim Opt1PopUpCell As Range
Set Opt1PopUpCell = opt1SelectCell.OFFSET(1, 0)
DeleteAllOpt1PopUps selectedCell
'--- now create listbox
Const OPT1_POPUP_WIDTH As Double = 75
Const OPT1_POPUP_HEIGHT As Double = 110
Const OPT1_OFFSET As Double = 5#
Dim opt1Box As ListBox
Set opt1Box = ActiveSheet.ListBoxes.Add(Opt1PopUpCell.Left + OPT1_OFFSET, _
Opt1PopUpCell.Top + OPT1_OFFSET, _
OPT1_POPUP_WIDTH, _
OPT1_POPUP_HEIGHT)
With opt1Box
.ListFillRange = Opt1ListArea().Address(external:=True)
.LinkedCell = ""
.MultiSelect = xlSimple
.Display3DShading = True
.OnAction = "Module1.Opt1BoxClick"
End With
'--- is there an existing list of options selected?
Dim selectedOptions1() As String
selectedOptions1 = Split(opt1SelectCell.Value, ",")
Dim opt1 As Variant
For Each opt1 In selectedOptions1
Dim i As Long
For i = 1 To opt1Box.ListCount
If opt1Box.List(i) = opt1 Then
opt1Box.Selected(i) = True
Exit For
End If
Next i
Next opt1
End Sub
This is an example of the excel data.
How can I make this work and even improve it?

Function return an error #Value! when I try to use it

I would like ask what's the reason to appear an error when I want return the value of my function
Public Function Alphabet_SEF() As Integer
Dim AllAreAlphabetic As Boolean
Dim ReturnVal As Integer
AllAreAlphabetic = True
Sheets("BlaBla").Activate
For i = 1 To Sheets("BlaBla").Range("E1", Range("E1").End(xlDown)).Rows.Count
If (VarType(Range("E1")) <> 8) Then
AllAreAlphabetic = False
Exit For
End If
Next
Sheets("CdM").Activate
If (AllAreAlphabetic) Then
ReturnVal = 1
Else
ReturnVal = 0
End If
Alphabet_SEF = ReturnVal
End Function
When I put in my exel book "=Alphabet_SEF()" appear #value!
Try this - it does not rely on BlaBla being active
Public Function Alphabet_SEF() As Boolean
Dim rng As Range, c As Range
Application.Volatile 'forces recalculation: use when you have no parameters for
' Excel to use to determine when it needs to be recalculated
With Sheets("BlaBla")
Set rng = .Range(.Range("E1"), .Cells(.Rows.Count, "E").End(xlUp))
End With
For Each c In rng.Cells
If VarType(c) <> 8 Then
Alphabet_SEF = False 'set to false and exit function
Exit Function
End If
Next
Alphabet_SEF = True 'if got here then all values are type 8
End Function

How to allow multiple successive undos in excel vba?

I have an excel workbook that needs to allow the user to undo multiple changes within a worksheet. I have searched online in every forum that I can think of and have not been able to find an answer for this. I realize that there is an issue with the undo issue in excel when macro's are run, and have been able to handle this using code derived from here.
This is my current process:
Create global variables to hold the initial state of the workbook, and the changes. Code is as follows:
Private Type SaveRange
Val As Variant
Addr As String
End Type
Private OldWorkbook As Workbook
Private OldSheet As Worksheet
Private OldSelection() As SaveRange
Private OldSelectionCount As Integer
Private InitialState() As SaveRange
Private InitialStateCount As Integer
Get the initial state of the workbook by building an array (InitialState) holding the values of all the cells in the Workbook_Open sub. Code is as follows:
Private Sub Workbook_Open()
GetInitialCellState
End Sub
Private Sub GetInitialCellState()
Dim i As Integer, j As Integer, count As Integer
Dim cellVal As String
Dim sampID As Range, cell As Range
Dim e1664 As Workbook
Dim rawData As Worksheet
Dim table As Range
Dim LastRow As Integer, LastCol As Integer
LastRow = Worksheets("Raw_Data").Range("A65536").End(xlUp).Row
LastCol = Worksheets("Raw_Data").UsedRange.Columns.count
Set e1664 = ThisWorkbook
Set rawData = e1664.Sheets("Raw_Data")
Set sampID = rawData.Range("SAMPLEID").Offset(1)
Set table = rawData.Range(sampID, "R" & LastRow)
i = 0
j = 0
count = 0
ReDim InitialState(i)
For i = 0 To (LastRow - sampID.Row)
For j = 0 To LastCol
ReDim Preserve InitialState(count)
InitialState(count).Addr = sampID.Offset(i, j).address
InitialState(count).Val = sampID.Offset(i, j).Value
count = count + 1
Next j
Next i
InitialStateCount = count - 1
End Sub
When a value is entered into a cell, store the value entered into another array (OldSelection) holding the value entered. This is done in the Workbook_Change sub. The important parts here are the Call SaveState(OldSelectionCount, Target.Cells.address, Target.Cells.Value) and Application.OnUndo "Undo the last action", "GI.OR.E1664.20150915_DRAFT.xlt!Sheet1.RevertState" pieces which are shown in numbers 4 and 5 below. Code is as follows:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim cell As Range, InWtRange As Boolean
Dim y As Integer, x As Integer, count As Integer
Dim LastRow As Integer
'This saves the changed values of the cells
Call SaveState(OldSelectionCount, Target.Cells.address, Target.Cells.Value)
try:
y = Me.Range("SampleID").Row
If Target.Column > 5 And Target.Column < 8 Then
If Range("A" & Target.Row).Value = Range("A" & Target.Row + 1).Value Then
If Range("A" & Target.Row + 1).Value <> "" Then
Range(Target.address).Offset(1).Value = Range(Target.address).Value
End If
End If
Else
'If initial pan weight add start date
If Target.Column = 8 Then
If Target.Cells.Text <> "" Then
If Not IsNumeric(Target.Cells.Value) Then
GoTo Finally
Else
Application.EnableEvents = False
Range("StartDate").Offset(Target.Cells.Row - y).Value = Format(Now(), "MM/DD/YY HH:NN:SS")
Application.EnableEvents = True
End If
Else
Application.EnableEvents = False
Range("StartDate").Offset(Target.Cells.Row - y).Value = ""
Application.EnableEvents = True
End If
End If
End If
LastRow = Worksheets("Raw_Data").Range("A65536").End(xlUp).Row
For Each cell In Target.Cells
'Debug.Print Target.Cells.Address
If cell.Value <> "" Then
If Not IsNumeric(cell.Value) Then GoTo Finally
Select Case cell.Column
Case 9, 11, 13
Application.EnableEvents = False
If CalcHEM(cell.Row - y, cell.Column) Then
End If
Application.EnableEvents = True
Case Else
'Do nothing yet
End Select
'Cells(Target.Row + 1, Target.Column).Select
End If
Next
'This will allow the changed values to be undone
Application.OnUndo "Undo the last action", "GI.OR.E1664.20150915_DRAFT.xlt!Sheet1.RevertState"
Finally:
If Application.EnableEvents = False Then Application.EnableEvents = True
Exit Sub
Catch:
MsgBox "An error has occurred in the code execution." & vbNewLine _
& "The message text of the error is: " & Error(Err), vbInformation, "TSSCalcs.AddQC"
Resume Finally
End Sub
The SaveState Sub will save add to the OldSelection array, any values that have changed. Code is as follows:
Private Sub SaveState(count As Integer, Addr As String, Val As Double)
Dim i As Integer
Dim cell As Range
If TypeName(Selection) <> "Range" Or Selection.count > 1 Then Exit Sub
ReDim Preserve OldSelection(count)
Set OldWorkbook = ActiveWorkbook
Set OldSheet = ActiveSheet
For Each cell In Selection
OldSelection(count).Addr = Addr
OldSelection(count).Val = Val
Next cell
OldSelectionCount = OldSelectionCount + 1
End Sub
The RevertState Sub will undo ONLY THE LAST ACTION! I am unable to allow more than the last entry to be undone. Code is as follows:
Private Sub RevertState()
Dim i As Integer, index As Integer
Dim prevItem As SaveRange
Dim address As String
OldWorkbook.Activate
OldSheet.Activate
Application.EnableEvents = False
address = OldSelection(OldSelectionCount - 1).Addr
OldSelectionCount = OldSelectionCount - 2
If OldSelectionCount <= 0 Then
ReDim OldSelection(0)
For i = 0 To InitialStateCount
If InitialState(i).Addr = address Then
prevItem.Val = InitialState(i).Val
index = i
End If
Next i
Range(InitialState(index).Addr).Formula = prevItem.Val
Else
ReDim Preserve OldSelection(OldSelectionCount)
For i = 0 To OldSelectionCount
If OldSelection(i).Addr = address Then
prevItem.Val = OldSelection(i).Val
index = i
End If
Next i
'OldSelectionCount = OldSelectionCount + 1
Range(OldSelection(index).Addr).Formula = prevItem.Val
End If
OldSelectionCount = OldSelectionCount + 1
Application.EnableEvents = True
End Sub
Does anyone know of a way to allow multiple undo's to be done?
Any help to solve this issue would be greatly appreciated!
After researching the Undo function on MSDN here, I found that the Application.Undo function only undoes the last action taken by the user. Instead of trying to get Microsoft's undo functionality to work, I have added my own undo and redo buttons which function the same as Microsoft's buttons. I have added two class modules: ActionState (holds the properties for workbook, worksheet, address and value of a cell)
ActionStates (a collection ActionState objects along with functions for adding, removing, getting an item, clearing the collection, counting, and properties for the CurrentState, and InitialState of the worksheet).
The new process is as follows:
Get the initial state of all the cells in the worksheet and add these to the undo stack array (see GetInitialCellStates() method within UndoFuntionality module).
When an item is added to a cell, add the address and value to the array (see SaveState() method within UndoFunctionality module) and update the index of the current state to the most recently added value. Repeat this step with any additional values.
When this is done, it enables the undo button.
If the undo button is pressed, it will decrement the index of the current state and enable the redo button (see RevertState() function within UndoFunctionality module).
If the redo button is pressed it will increment the index of the current state (see ProgressState() function within UndoFunctionality module).
The code for the ActionState class is as follows:
Private asAddr As String
Private asVal As Variant
Private asWorkbook As Workbook
Private asWorksheet As Worksheet
Private Sub Class_Initalize()
Set asWorkbook = New Workbook
Set asWorksheet = New Worksheet
End Sub
'''''''''''''''''''
' Addr property
'''''''''''''''''''
Public Property Get Addr() As String
Addr = asAddr
End Property
Public Property Let Addr(Value As String)
asAddr = Value
End Property
'''''''''''''''''''
' Val property
'''''''''''''''''''
Public Property Get Val() As Variant
Val = asVal
End Property
Public Property Let Val(Value As Variant)
asVal = Value
End Property
'''''''''''''''''''
' Wkbook property
'''''''''''''''''''
Public Property Get Wkbook() As Workbook
Set Wkbook = asWorkbook
End Property
Public Property Let Wkbook(Value As Workbook)
Set asWorkbook = Value
End Property
'''''''''''''''''''
' WkSheet property
'''''''''''''''''''
Public Property Get Wksheet() As Worksheet
Set Wksheet = asWorksheet
End Property
Public Property Let Wksheet(Value As Worksheet)
Set asWorksheet = Value
End Property
The code for the ActionStates class is as follows:
Private asStates As Collection
Private currState As Integer
Private initState As Integer
Private Sub Class_Initialize()
Set asStates = New Collection
End Sub
Private Sub Class_Termitate()
Set asStates = Nothing
End Sub
''''''''''''''''''''''''''''
' InitialState property
''''''''''''''''''''''''''''
Public Property Get InitialState() As Integer
InitialState = initState
End Property
Public Property Let InitialState(Value As Integer)
initState = Value
End Property
''''''''''''''''''''''''''''
' CurrentState property
''''''''''''''''''''''''''''
Public Property Get CurrentState() As Integer
CurrentState = currState
End Property
Public Property Let CurrentState(Value As Integer)
currState = Value
End Property
''''''''''''''''''''''''''''
' Add method
''''''''''''''''''''''''''''
Public Function Add(Addr As String, Val As Variant) As clsActionState
Dim asNew As New clsActionState
With asNew
.Addr = Addr
.Val = Val
End With
asStates.Add asNew
End Function
''''''''''''''''''''''''''''
' Count method
''''''''''''''''''''''''''''
Public Property Get count() As Long
If TypeName(asStates) = "Nothing" Then
Set asStates = New Collection
End If
count = asStates.count
End Property
''''''''''''''''''''''''''''
' Item method
''''''''''''''''''''''''''''
Public Function Item(index As Integer) As clsActionState
Set Item = asStates.Item(index)
End Function
''''''''''''''''''''''''''''
' Remove method
''''''''''''''''''''''''''''
Public Function Remove(index As Integer)
If TypeName(asStates) = "Nothing" Then
Set asStates = New Collection
End If
asStates.Remove (index)
End Function
''''''''''''''''''''''''''''
' Clear method
''''''''''''''''''''''''''''
Public Sub Clear()
Dim x As Integer
For x = 1 To asStates.count
asStates.Remove (1)
Next x
End Sub
These two classes are used in a new module called UndoFunctionality as follows:
Option Explicit
Public ActionState As New clsActionState
Public ActionStates As New clsActionStates
Public undoChange As Boolean
Public Sub SaveState(count As Integer, Addr As String, Val As Variant)
Dim i As Integer
Dim cell As Range
If TypeName(Selection) <> "Range" Or Selection.count > 1 Then Exit Sub
With ActionState
.Wkbook = ActiveWorkbook
.Wksheet = ActiveSheet
End With
If ActionStates.CurrentState < ActionStates.count Then
For i = ActionStates.CurrentState + 1 To ActionStates.count
ActionStates.Remove (ActionStates.count)
Next i
End If
For Each cell In Selection
ActionState.Addr = Addr
ActionState.Val = Val
Next cell
ActionStates.Add ActionState.Addr, ActionState.Val
ActionStates.CurrentState = ActionStates.count
End Sub
Public Sub RevertState()
Dim i As Integer, index As Integer
Dim prevItem As New clsActionState
Dim Address As String
'undoChange = True
With ActionState
.Wkbook.Activate
.Wksheet.Activate
End With
Application.EnableEvents = False
Address = ActionStates.Item(ActionStates.CurrentState).Addr
ActionStates.CurrentState = ActionStates.CurrentState - 1
For i = 1 To ActionStates.CurrentState
If ActionStates.Item(i).Addr = Address Then
prevItem.Val = ActionStates.Item(i).Val
index = i
End If
Next i
Range(ActionStates.Item(index).Addr).Formula = prevItem.Val
Application.EnableEvents = True
UndoButtonAvailability
RedoButtonAvailability
End Sub
Public Sub ProgressState()
Dim i As Integer, index As Integer
Dim nextItem As New clsActionState
Dim Address As String
With ActionState
.Wkbook.Activate
.Wksheet.Activate
End With
Application.EnableEvents = False
ActionStates.CurrentState = ActionStates.CurrentState + 1
With nextItem
.Addr = ActionStates.Item(ActionStates.CurrentState).Addr
.Val = ActionStates.Item(ActionStates.CurrentState).Val
End With
Range(ActionStates.Item(ActionStates.CurrentState).Addr).Formula = nextItem.Val
Application.EnableEvents = True
UndoButtonAvailability
RedoButtonAvailability
End Sub
Public Sub GetInitialCellStates()
Dim i As Integer, j As Integer, count As Integer
Dim cellVal As String
Dim sampID As Range, cell As Range
Dim e1664 As Workbook
Dim rawData As Worksheet
Dim table As Range
Dim LastRow As Integer, LastCol As Integer
ThisWorkbook.Worksheets("Raw_Data").Activate
If ActionStates.count > 0 Then
ActionStates.Clear
End If
LastRow = Worksheets("Raw_Data").Range("A65536").End(xlUp).Row
LastCol = Worksheets("Raw_Data").UsedRange.Columns.count
Set e1664 = ThisWorkbook
Set rawData = e1664.Sheets("Raw_Data")
Set sampID = rawData.Range("SAMPLEID").Offset(1)
Set table = rawData.Range(sampID, "R" & LastRow)
i = 0
j = 0
count = 0
For i = 0 To (LastRow - sampID.Row)
For j = 0 To LastCol
ActionState.Addr = sampID.Offset(i, j).Address
ActionState.Val = sampID.Offset(i, j).Value
ActionStates.Add ActionState.Addr, ActionState.Val
count = count + 1
Next j
Next i
ActionStates.InitialState = count
ActionStates.CurrentState = count
undoChange = False
UndoButtonAvailability
RedoButtonAvailability
End Sub
Public Sub UndoButtonAvailability()
Dim rawData As Worksheet
Set rawData = ThisWorkbook.Sheets("Raw_Data")
If ActionStates.CurrentState <= ActionStates.InitialState Then
rawData.Buttons("UndoButton").Enabled = False
rawData.Buttons("UndoButton").Font.ColorIndex = 16
Else
rawData.Buttons("UndoButton").Enabled = True
rawData.Buttons("UndoButton").Font.ColorIndex = 1
End If
End Sub
Public Sub RedoButtonAvailability()
Dim rawData As Worksheet
Set rawData = ThisWorkbook.Sheets("Raw_Data")
If ActionStates.CurrentState < ActionStates.count Then
rawData.Buttons("RedoButton").Enabled = True
rawData.Buttons("RedoButton").Font.ColorIndex = 1
Else
rawData.Buttons("RedoButton").Enabled = False
rawData.Buttons("RedoButton").Font.ColorIndex = 16
End If
End Sub
Sub UndoButton_Click()
Dim rawData As Worksheet
Set rawData = ThisWorkbook.Sheets("Raw_Data")
If rawData.Buttons("UndoButton").Enabled Then
RevertState
End If
End Sub
Sub RedoButton_Click()
Dim rawData As Worksheet
Set rawData = ThisWorkbook.Sheets("Raw_Data")
If rawData.Buttons("RedoButton").Enabled Then
ProgressState
End If
End Sub
The GetInitialStates method is used in the workbook_open event as follows:
UndoFunctionality.GetInitialCellStates
And the Worksheet_Change event within the worksheet is as follows:
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
Dim cell As Range, InWtRange As Boolean
Dim y As Integer, x As Integer, count As Integer
Dim LastRow As Integer
'This saves the changed values of the cells
Call SaveState(ActionStates.CurrentState, Target.Cells.Address, Target.Cells.Value)
try:
y = Me.Range("SampleID").Row
If Target.Column > 5 And Target.Column < 8 Then
If Range("A" & Target.Row).Value = Range("A" & Target.Row + 1).Value Then
If Range("A" & Target.Row + 1).Value <> "" Then
Range(Target.Address).Offset(1).Value = Range(Target.Address).Value
End If
End If
Else
'If initial pan weight add start date
If Target.Column = 8 Then
If Target.Cells.Text <> "" Then
If Not IsNumeric(Target.Cells.Value) Then
GoTo Finally
Else
Application.EnableEvents = False
Range("StartDate").Offset(Target.Cells.Row - y).Value = Format(Now(), "MM/DD/YY HH:NN:SS")
Application.EnableEvents = True
End If
Else
Application.EnableEvents = False
Range("StartDate").Offset(Target.Cells.Row - y).Value = ""
Application.EnableEvents = True
End If
End If
End If
LastRow = Worksheets("Raw_Data").Range("A65536").End(xlUp).Row
For Each cell In Target.Cells
If cell.Value <> "" Then
If Not IsNumeric(cell.Value) Then GoTo Finally
Select Case cell.Column
Case 9, 11, 13
Application.EnableEvents = False
If CalcHEM(cell.Row - y, cell.Column) Then
End If
Application.EnableEvents = True
Case Else
'Do nothing yet
End Select
End If
Next
UndoFunctionality.UndoButtonAvailability
UndoFunctionality.RedoButtonAvailability
Finally:
If Application.EnableEvents = False Then Application.EnableEvents = True
Exit Sub
Catch:
MsgBox "An error has occurred in the code execution." & vbNewLine _
& "The message text of the error is: " & Error(Err), vbInformation, "TSSCalcs.AddQC"
Resume Finally
End Sub
The only thing left is to add two buttons to the worksheet and assign the macro used to the UndoButton_Click() and RedoButton_Click() events which will run the RevertState() and ProgressState() methods.
I found a little trick using Application.OnTime. So it is possible to use Undo repeatedly.
The Repeat button is not the Redo button. You can find it in the Edit menu or put it on your ribbon.
I am using Excel 2003.
Here is a working sample. Put the code inside ThisWorkbook module.
Dim Undos As New Collection
Sub Change()
' push previous cell values to the end of your undo array
Undos.Add ActiveCell.Value
' change the cell values as you wish
ActiveCell.Value = "(" + ActiveCell.Value + ")"
PlanUndo
PlanRepeat
End Sub
Sub Undo()
' make sure the undo array is not empty
If (Undos.Count > 0) Then
' pop previous cell values from the end of your undo array
Dim Value
Value = Undos.Item(Undos.Count)
Undos.Remove Undos.Count
' revert the cell values
ActiveCell.Value = Value
End If
If (Undos.Count > 0) Then
PlanUndo
End If
PlanRepeat
End Sub
Function PlanUndo()
Application.OnTime Now, "ThisWorkbook.SetUndo"
End Function
Sub SetUndo()
Application.OnUndo "Undo last change", "ThisWorkbook.Undo"
End Sub
Function PlanRepeat()
Application.OnTime Now, "ThisWorkbook.SetRepeat"
End Function
Sub SetRepeat()
Application.OnRepeat "Repeat last change", "ThisWorkbook.Change"
End Sub

Resources