Updating a macro to be identical across all worksheets (or making the code more global?) - vba

I have a workbook with a few dozen worksheets, which is growing. I might also end up with copies of this workbook pretty soon.
Each sheet is based on the same worksheet template, which includes a macro on its Worksheet_Change event for auto-calculations. As I improve the workbook and add capabilities, and sheets are added, it's taking longer and longer to copy-paste the updated macro to all sheets.
I'm wondering if there's a way to either:
Make another macro that copies the updated version of the macro from the template to all other worksheets, overwriting the old versions in the process
And/or
Move the code from worksheet_change to a more global place where I'd only need to update one version of it per workbook (which is fine and much better than updating 20+ worksheets manually across soon-to-be 3 workbooks...)
Solution #2 would be better because more elegant I think? But I have no clue if it's possible. Barring that I'd gladly take #1 for the time-saving aspect!
Thank you in advance.

If we are talking about one workbook with multiple worksheets, then an easy approach (which solves the updating issue) would be:
Add a Module and write a procedure containing the original change events code:
Option Explicit
Public Sub MyGlobalWorksheet_Change(ByVal Target As Range)
' here the code from your orignal Worksheet_Change.
' make sure you reference worksheets correctly
' the worksheet can eg be addressed like
' set ws = Target.Parent
End Sub
So in your worksheets you only need to add a generic call like
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
MyGlobalWorksheet_Change Target
End Sub
to call the global procedure. Therefore the Worksheet_Change event never needs to be changed, however you just need to add it once.
Whenever you need to change something at the code you just need to change one procedure which is MyGlobalWorksheet_Change and it affects all your desired sheets at once (but only sheets you added the call to your global event).
Remember it is always a bad idea to copy the same code over and over again, because it is hard to maintain. Instead always use one procedure you call again and again.
Another way would be using the Workbook_SheetChange event within the ThisWorkbook scope. But this will affect any sheet within the workbook. The previous solution will only affect the workbooks you choose by adding a call.

Related

Referencing the selection in deactivated Workbook

Private Sub Workbook_Deactivate()
ThisWorkbook.ActiveSheet.Selection.Copy
End Sub
I'd like to copy the selection in the deactivated workbook. But this doesn't work, indeed, I got a run-time error.
Damn! It is possible!
Private Sub Workbook_Deactivate()
ThisWorkbook.Windows(1).Selection.Copy
End Sub
Explanation:
Both ActiveSheet and Selection are children objects of the Application object, not the Workshet or Workbook objects. (See MSDN: Selection and ActiveSheet.) Selection means the currently selected item in the active window. Same for ActiveSheet.
Now it turns out that you can specify a window object for both of them. Normally every Workbook has one window, but you can create multiple windows for each open workbook. Anyhow, every open workbook that you can click and select will have at least one window. This is whateverworkbook.Windows(1).
ThisWorkbook always references the workbook running the macro (that means the workbook actually containing the VBA code). In your case, that is the one you are leaving. (Unless you want the macro to run from any workbook you are deactivating, in which case you'd have to come up with a custom event handler that catches each workbook deactivate event...)
And that's it, really. You can reference a selection in another workbook, you just have to attack it through it's Window.
Thanks for the good question, I have learned something very valuable today. :)

More than one workbook object in project explorer

I have this strange problem. I have a vba project, where the project explorer displays more than one workbook object. It is exactly the same as in this SO question, but I don't have any faulty references to uncheck.
However, I do know what's caused it and I'm sure you can all duplicate. What I did was use the codename for a sheet as a byref argument for a simple sub and at the end of the sub, nulled the worksheet object. So I nulled the whole sheet through the reference.
Something like:
Option Explicit
Sub test_1()
test_2 sh:=Sheet2
End Sub
Sub test_2(ByRef sh As Worksheet)
Set sh = Nothing
End Sub
If you run test_1, it will run without problems. But after that, the 'Sheet2' codename is invalid. And if you close and reopen the workbook, you'll see what I mean. Excel creates a new worksheet with the same name (but another CodeName). Any data stored in the cells of the sheet is not lost. The old CodeName references the Workbook Object.
I have not found a way to restore or remove the old references so far (other then move all the objects to a new workbook). I am using Excel 2013.
Solution is of course not so null the worksheet, but does anyone have any idea how to restore?
Well, after suffering this weird behaviour of Excel I can finally say that I have tried man times and that I can confirm that the cause (at least for my experiments) was indeed using User Defined Functions (UDFs). There is no way of fixing a Workbook once the extrange sheets appear. My approach was to just create a new Workbook and copy all the Modules and all the sheets from the old (broken) one.
Just make sure to not use user defined functions if you are having this issue.

Saving values upon excel cell deletion

So, I'm back with a more specific answer/problem.
Also, I'm sorry, I picked up excel (hadn't actually used the program) a few weeks back only and minimal usage. So if the code is bad/explanations unclear, comment and i'll do my best to get this together.
I am currently in a situation with two separate (albeit linked) excel files.
Src (Book1)
Dest (Book2)
They each have one worksheet.
The idea is that Src will be modified (including deletion and insertion of rows) and i need these changes to be reflected onto Dest.
I have the insertion so far, scrounged up a little bit of code using Before_DoubleClick.
However i am having a few issues on deletion.
What i have this far is :
Private Sub Worksheet_Change(ByVal Target As Range)
Const destBook As String = "Book2"
Const destSheet As String = "Sheet2"
Dim wb1 As Workbook
Set wb1 = Workbooks(destBook)
' Here is the main problem i have. I need a better way to detect
' deletion of one or more rows. Otherwise the Application.Undo
' re-enters the if and it just loops.
If Target.Address = Target.EntireRow.Address Then
' Here i Undo the deletion to be able to copy the values.
Application.Undo
Target.Copy
' I also haven't found a way to delete again but google can help me on that
' Pasting line
'wb1.Sheets(destSheet).Cells(Target.Row, 1).PasteSpecial Paste:=xlPastValues
End If
End Sub
Ideally, what should happen is, upon deletion of one or more rows, i would like to undo the last action, copy the data, delete the row and finally past the data in another worksheet. However, as of now i am blocked at undoing the last action. It calls my VBA macro again and just starts looping.
Im not sure what you want but you either have to replace formulas with values in the new cells, or if those cells are supposed to have a formula you probably have to do
application.Calculation=xlCalculationManual
make the deletion or change then
application.Calculation=xlCalculationAutomatic
to turn autocalculation back on
Manual is tricky because if code crashed you would want to make sure you didn't leave the sheet in a state that didn't calculate

Save undo stack during macro run

I am wondering if there is a way to save ability to undo actions after macro has been run.
I do not care about results of macro - just need to undo actions that were done by user before macro.
Background:
I have a macro on the worksheet_change event that logs who and when made the change on this worksheet. I do not want it to restrict user's ability to undo his/her actions.
There is no easy way to do this, but it's possible. The approach to this is to create three macros, and use some global variables to save state:
MyMacro
MyStateSavingMacro
MyStateRevertingMacro
E.g. My macro changes Cells in Range A1:A10 of the active sheet. So, whenever the code to run my macro is called, it executes
Sub MyMacro()
Call MyStateSavingMacro()
' Copies contents and formulae in range A1:A10 to a global data object
'... Code for MyMacro goes here
'
'................
Call Application.OnUndo("Undo MyMacro", "MyStateRevertingMacro")
'This puts MyStateRevertingMacro() in the Undo queue
'So pressing ctrl-Z invokes code in that procedure
End Sub
Sub MyStateSavingMacro()
' Code to copy into global data structures anything you might change
End Sub
Sub MyStateRevertingMacro
' Code to copy onto the spreadsheet the original state stored in the global variables
End Sub
So there it is. It's not pretty, but can be done.
Ref: http://msdn.microsoft.com/en-us/library/office/ff194135%28v=office.15%29.aspx
Edit:
To preserve the Undo queue prior to your MyMacro being run, the inelegant solution would be to create a chain of 4-5 MyStateRevertingMacro_1, _2, etc. where you can apply the information from your Worksheet_Change logging system and then chain-up the Application.OnUndo in each of those, so Application.OnUndo for each of those Reverting Macros would refer the previous state reversion code.
You can use the hidden mirror sheet to do this. Of course it will work if your worksheet is simple enough. You must decide which cells are editable and copy them TO mirror sheet, which are not editable and copy them FROM mirror sheet. And your macro should work only in the mirror sheet. That's it.
This one is a bit old now but in case anyone is still trying to get this to work - I just tried setting the undo stack to be able undo the formatting on a column that a macro had reformatted and noticed that by doing that the full undo command worked (before I added this bit the undo was unavailable) - does not matter what the custom undo code contains (in my case I had not even created the routine yet), the application undo works perfectly
Application.OnUndo "Undo Amount Format", "sUndo_Col2"

VBA logic when using macros from personal.xls

I run a spreadsheet report that holds about 50 columns of data for anywhere from 1 to 5000 rows. I'm only interested in 4 columns, but they are never in the same location as these reports are set-up a bit differently for each client. I then take those 4 columns and paste into a new workbook that I can import into another program.
I have three macros created that accomplish this task flawlessy if ran from the local file. When I load them into the personal.xls for use on various files I have issues. Specifically workbook/worksheet referencing issues.
Parts of the macro run to the sheet I intend from them to result on, while other parts act on the personal.xls file itself. This confuses me because I don't have any lines that use commands such as 'thisworkbook' or 'activeworksheet'.
For example:
- The first line is coded to rename Sheet1. The macro renames Sheet1 in personal.xls.
- The next line is the first of four Find commands that locate where the columns i'm interested are located and then move them. This macro runs perfectly on the sheet I intend.
I think my best course is to begin each macro by naming the active workbook and then breaking out each command to the workbook level instead of starting with Worksheets, Range, etc.
Can anyone help me understand what VBA is thinking when performing macros from personal.xls and how to best avoid the macros being run on that sheet itself?
There are two approaches you can take. I use one or both in my code - it's not a one or the other situations.
Declare Variables
Start by defining each sheet that you want to work on in a variable. I generally stay at the sheet level, but that's just a personal choice. If you'd rather be at the workbook level, that's OK too. A procedure might looks like:
Dim shSource as Worksheet
Dim shDest as Worksheet
Set shSource = Workbooks("SomeBook").Worksheets(1)
Set shDest = ActiveWorkbook.Worksheets("Summary")
then whenever I reference a Range or Cells or anything else on a sheet, I preface it with that sheet object variable. Even if I need to get to the workbook, I start with the sheet. If I needed to, for instance, close the Source workbook from the above example, I would use
shSource.Parent.Close False
I set up the sheet variables I need and then everything I do is in terms of those variables.
Edit
If you're opening or creating workbooks, then variables is definitely the way to go. For example, if you're opening a workbook, you could use one of these two examples
Dim wb As Workbook
Set wb = Workbooks.Open(C:\...)
Dim ws As Worksheet
Set ws = Workbooks.Open("C:\...).Worksheets(1)
or creating new, one of these two examples:
Dim wb As Workbook
Set wb = Workbooks.Add
Dim ws as Worksheet
Set ws = Workbooks.Add.Worksheets(1)
With Blocks
When I'm only trying to get at something one time, it seems like a waste to set up a bunch of variables. In those cases, I use a With Block so I can still have fully qualified references, but without a bunch of clutter in my code.
With Workbook("MyBook")
With .Worksheets("First_Sheet")
.Range("A1").Value = "stuff"
End With
With .Worksheets("Second_Sheet")
.Range("G10").Formula = "=A1"
End With
End With
I probably prefer the variable method, but I use them both.
Edit 2: Implicit Referencing
You should always explicitly reference your workbooks and worksheets, but it's still instructional to know how Excel will behave if you don't. A line of code that starts like Range("A1").Value = ... is called an unqualified reference. You're referencing a range, but you're not saying which sheet its on or which workbook that sheet is in. Excel handles unqualified references differently depending on where your code is.
In a Sheet's Class Module (like where you use sheet events like SelectionChange), unqualified references refer to the sheet represented by that module. If you're in the Sheet1 module working in the Change event and you code x = Range("G1").Value then the G1 you are referring to is on Sheet1. In this case, you should be using the Me keyword rather than relying on Excel.
In any other module (like a Standard Module), unqualified references refer to the ActiveSheet. The same x = Range("G1").Value code in a Standard Module refers to G1 on whichever sheet has the focus.
Excel's treatment of unqualified references is very reliable. You could easily create robust code by relying on Excel to resolve the qualified references. But you shouldn't. Your code will be more readable and easier to debug if you qualify every reference. I qualify every reference. And that's not one of those things I "always" do except when I'm lazy - I really do it 100% of the time.