Copying sheets while preserving digital signatures - vba

-- Edit: this is now part of the bigger question of how to reliably move sheets about in this question's context --
(Note: during preparing this post and testing solutions, I probably have already answered my own question. Just posting this in the hope anyone smarter than me can come up with something. Anyway, it's still a good resource for future searchers I guess.)
Problem description
I made an Excel solution for one of my customers which has tons of VBA in it. I therefore naturally signed the VBA code, so my customer doesn't get the macro security messages. However, one thing this solution does is making copies of a template sheet in the same workbook. The template sheet is found on it's code name, and all copies of the sheet are from then on recognized by their code name being derived from this (having a trailing sequence nr.) - they need to be identified and handled later on again.
Quite innocent on first sight, but when I demoed the solution and tried to save it I instantly got:
"You have modified a signed project. You do not have the correct key
to sign this project. The signature will be discarded."
after which the signature was discarded, and on re-open the macro security prompts put themselves to good use. Not a good impression :(
The code goes like this in simplified form:
There is a (hidden) "template" sheet in the workbook that acts as the source for new sheets (it has no VBA code behind it nor any ActiveX or form controls on it);
A ribbon button calls VBA code that uses Worksheet.Copy to make a copy of this sheet (and modifies the copy, but that is irrelevant here);
On next save, Excel wants to discard the digital signature.
When I perform the same actions manually on a machine that doesn't have my certificate, I get the same experience. (A lesson: always test on truly blank systems before demoing anything...)
Possible cause
I've searched on this a bit (see e.g. ozgrid.com and answers.microsoft.com), and while remarkedly few people run into this, it seems like a sort-of inevitable thing. The reason behind it I suspect goes like this:
Although the template sheet has no 'real' VBA code on it, the VBA module does exist and has some not-insignificant content;
Copying this sheet creates a new sheet with a thus seemingly 'empty' but still existing and thus significant VBA module;
The hash of the 'total' VBA project is thus altered and the signature is lost.
According to the post on ozgrid.com, this also happens on deletion of sheets, which is explained by the above. It also suggests creating new sheets without the VBA IDE open doesn't trigger this, and deleting these new sheets works too. But once you go to the VBA IDE, all sheets currently present become 'non-deletable' again.
I suspect that when you add a new worksheet without the VBA editor open, Excel adds a worksheet with truly no VBA module added to it, so the project hash will not update. These sheets thus can also be deleted for the same reason. Opening the VBA editor in turn makes the IDE query for the modules in the workbook, at which time these still missing modules get created, baking their presence into the hash, which in turn also makes them uncopyable because their VBA footprint has become non-zero.
Solutions
Now the $1,000,000 question is: how can we work around this? There's some smart people on this site, so maybe we can come up with an out-of-the-box solution?
A useage detail that will make this easier (at least for me): the customer is the only one adding sheets, and he is never going to enter the IDE. It would be nice if I couldn't inadvertently mess up a build just by forgetfully entering the IDE, though.
I've already tried several possible solutions, creating them on a computer with my signature, and testing them on a computer without my signature. For now I'm using Excel 2010 32-bit exclusively for these tests, as that's either all I have, and it is also the version me and my customer are most interested in.
Non-solution 1
Delete all VBA code from the template sheet via the IDE, so it has no contribution to the hash.
If only it were so simple... This didn't work, so probably the existence of the module itself and/or it's meta-data (like it's name) is also hashed, which doesn't sound unreasonable. Or you simply cannot remove all VBA code since the IDE has the tendency to always append an empty line (so a single CrLf is as empty as you can make it this way, though it's CodeModule.CountOfLines return 0 on it). Or the entire VBA code module's content is retrieved and hashed, such that the terminating NULL char or leading 0 byte count contributes to the hash. Anyway, no luck here.
As a test I added a macro that tells which VBA modules there are, and how many lines they contain. Using this, a direct copy of the 'emptied' template sheet still has 0 lines but the signature is lost, while a newly inserted sheet shows up in the VBModules collection and even has 2 lines (the default Option Explicit) and the signature sticks nontheless on save...
But Excel might just be outsmarting us, with that 2-lined Option Explicit being a virtual one, or even the presence of the VBA module in the first place being virtual. When I made the macro also list all sheets with their code names, it turns out these 'safe' sheets have an empty code name (0-length string), indeed indicating they have no code module at all.
Non-solution 2
Create a fresh new sheet instead, and only copy over the contents of the template sheet.
While this does work, it seems a bit iffy to me; I do not believe a mere sourceSheet.Cells.Copy destSheet.Cells will copy absolutely everything the user can throw on it... I'd rather thus keep using the build-in Worksheet.Copy function to be safe and to not have to write piles of special code for every conceivable detail.
As a case on point: sourceSheet.Cells.Copy destSheet.Cells e.g. does copy over worksheet-specific named ranges, but apparently only if they're actually used on the sheet itself. Unreferenced names just vanish in the copy! Talk about special-case copy code I'd have to write...
And then there's the copied sheet not getting any code name assigned at all, which I currently need to recognize them.
Non-solution 3
Create a new temporary workbook, Worksheet.Copy the sheet to there, note it's name, explicitly save it as an .xlsx file to get rid of any VBA module, close and re-open the temp workbook to get rid of any old in-memory cruft, find it again by name, then Worksheet.Move it back to the source workbook.
This works! Without the actual workbook re-open it doesn't, so I guess the in-memory representation just cannot be 'scrubbed' easily enough to not do any harm.
However... The new sheet again doesn't get a code name at all, and even more: I do not like this sheet moving around to unrelated workbooks; while in a quick test any references to other sheets in the original workbook were conserved (and not even got expanded to include the workbook name or path!), I am still a bit uneasy about this... Who knows what type of content users might throw at it...
<Paranoid mode="on">And who knows what type of confidential information will be in there, which I do not want to have the responsibility for when it ends up leaking from the Temp folder without their knowing.</Paranoid>
Non-solution 4
Create a new, empty, temporary sheet as well as a Worksheet.Copy of the template, then replace the true copy's VBA module with the temporary sheet's one. Or just nuke the VBA module as a whole.
I just can't devise a way to do this. VBA itself won't let you do it it seems, and then again I do not want my customers to have to turn on the 'Allow access to the VB project' option for this alone. And I suspect were I able to do this, the damage would already have been done before I could nuke the code module again.
Non-solution 5
Create a macro that is only visible to me (the developer), that creates a perfect copy of the template sheet via either solution 2 or 3, and discards the original template sheet, replacing it with the VBA-scrubbed copy. To be used by me as the last step just before delivering it to the customer.
Solution 2's caveats are less important here because I do know myself what's on the template sheet when I make a new version delivery, so the amount of code needed for a perfect copy is minimal and can be controlled. But then 3 just seems safer and easier... I'll have to pick one.
Since I access the template sheet on it's VBA code name by just using shtTemplate. directly instead of ThisWorkbook.Worksheets("Template")., that apparently complicates it all too much for Excel to switch it in-and-out on the fly. All my attempts so far either failed or just made Excel crash hard on me. No love there :(
I tried this again by manipulating a copy loaded in a second Excel set to msoAutomationSecurityForceDisable, thus avoiding a running VBA host being undermined, also saving and re-opening after almost every update. But that led nowhere either, giving errors like "Automation error - Catastrophic failure" when opening the scrubbed workbook, or mightily corrupting the new workbook (the ThisWorkbook module being duplicated for each sheet module in the project explorer with a derived name).
Maybe-solution 6
Re-write all VBA to not use the hard-coded template sheet's code name, but storing this name on a settings sheet, then applying solution 5 above.
The code finally works, not even having to use a second staging Excel; no crashes nor corruptions! But this code works only insofar that I cannot for the life of me get the code to give the scrubbed sheet a valid code name again; it remains a zero-length string. And no run-time errors to indicate this either. When I have the IDE open during this, the code name is set correctly though.
Which leads me to believe that having a code name on your sheet implies it having a non-null code module, which implies it messing with the digital signature. And that's... not so unexpected really, in hindsight.
Final solution
Which leads me to believe there is just no way whatsoever that I could create a template sheet that both:
Is safe to copy via Worksheet.Copy without losing the signature, and
Has no code module while having a non-null code name.
The only solution I see so far is thus to indeed use a scrubbed template sheet to be able to use Worksheet.Copy, but to find and identify it and it's resulting sheets by other means than by their code name. There is a user-hidden section on it that I might add a "This is the template/copy" status to, though it makes my inner perfectionist cringe.
However, if anyone feels like experimenting, it would be nice to have a few more alternatives! I can post code samples when needed.

It's a lot to take in, and I do not pretnd this will answer will solve all your problems. But I once wrote a function called SoftLink which would take up to 4 parameters (i) Boolean: CellRef (or NamedRange) (ii) String: Range (iii) String: WorksheetName (iv) String: WorkbookName which would break any link with any cells and then you resolve the string parameters in VBA code.
There no doubt a performance hit with this approach but it is one way to solve Link hell.
Example calling formulas
=softlink(FALSE,"Foo")
=softlink(TRUE,"C4","Sheet1","Book2")
=softlink(TRUE,"D5","Sheet2")
and I have knocked up from memory an implementation. I have a phobia of On Errors .... so forgive some strange loopings in the subroutines.
Option Explicit
Function SoftLink(ByVal bIsCell As Boolean, ByVal sRangeName As String, _
Optional sSheetName As String, Optional sBookName As String) As Variant
Dim vRet As Variant
If Len(sRangeName) = 0 Then vRet = "#Cannot resolve null range name!": GoTo SingleExit '* fast fail
Dim rngCaller As Excel.Range
Set rngCaller = Application.Caller
Dim wsCaller As Excel.Worksheet
Set wsCaller = rngCaller.Parent
Dim wbCaller As Excel.Workbook
Set wbCaller = wsCaller.Parent
Dim wb As Excel.Workbook
If Len(sBookName) > 0 Then
vRet = FindWorkbookWithoutOnErrorResumeNext(sBookName, wb)
If Len(vRet) > 0 Then GoTo ErrorMessageExit
Else
Set wb = wbCaller
End If
Debug.Assert Not wb Is Nothing
Dim ws As Excel.Worksheet
If Len(sSheetName) > 0 Then
vRet = FindWorksheetWithoutOnErrorResumeNext(wb, sSheetName, ws)
If Len(vRet) > 0 Then GoTo ErrorMessageExit
Else
Set ws = wsCaller
End If
Dim rng As Excel.Range
If bIsCell Then
vRet = AcquireCellRange(ws, sRangeName, rng)
If Len(vRet) > 0 Then GoTo ErrorMessageExit
Else
vRet = AcquireNamedRangeWithoutOERN(ws, sRangeName, rng)
If Len(vRet) > 0 Then GoTo ErrorMessageExit
End If
SoftLink = rng.Value2
SingleExit:
Exit Function
ErrorMessageExit:
SoftLink = vRet
GoTo SingleExit
End Function
Function AcquireCellRange(ByVal ws As Excel.Worksheet, ByVal sRangeName As String, ByRef prng As Excel.Range) As String
On Error GoTo FailedCellRef
Set prng = ws.Range(sRangeName)
SingleExit:
Exit Function
FailedCellRef:
AcquireCellRange = "#Could not resolve range name '" & sRangeName & "' on worksheet name '" & ws.Name & "' in workbook '" & ws.Parent.Name & "'!"
End Function
Function AcquireNamedRangeWithoutOERN(ByVal ws As Excel.Worksheet, ByVal sRangeName As String, ByRef prng As Excel.Range) As String
'* because I do not like OERN
Dim oNames As Excel.Names
Dim bSheetScope As Long
For bSheetScope = True To False
Set oNames = VBA.IIf(bSheetScope, ws.Names, ws.Parent.Names)
Dim namLoop As Excel.Name
For Each namLoop In oNames
If VBA.StrComp(namLoop.Name, sRangeName, vbTextCompare) = 0 Then
Set prng = ws.Range(sRangeName)
GoTo SingleExit
End If
Next
Next
ErrorMessageExit:
AcquireNamedRangeWithoutOERN = "#Could not resolve range name '" & sRangeName & "' on worksheet name '" & ws.Name & "' in workbook '" & ws.Parent.Name & "'!"
SingleExit:
Exit Function
End Function
Function FindWorksheetWithoutOnErrorResumeNext(ByVal wb As Excel.Workbook, ByVal sSheetName As String, ByRef pws As Excel.Worksheet) As String
'* because I do not like OERN
Dim wsLoop As Excel.Worksheet
For Each wsLoop In wb.Worksheets
If VBA.StrComp(wsLoop.Name, sSheetName, vbTextCompare) = 0 Then
Set pws = wsLoop
GoTo SingleExit
End If
Next wsLoop
ErrorMessageExit:
FindWorksheetWithoutOnErrorResumeNext = "#Could not resolve worksheet name '" & sSheetName & "' in workbook '" & wb.Name & "'!"
SingleExit:
Exit Function
End Function
Function FindWorkbookWithoutOnErrorResumeNext(ByVal sBookName As String, ByRef pwb As Excel.Workbook) As String
'* because I do not like OERN
Dim wbLoop As Excel.Workbook
For Each wbLoop In Application.Workbooks
If VBA.StrComp(wbLoop.Name, sBookName, vbTextCompare) = 0 Then
Set pwb = wbLoop
GoTo SingleExit
End If
Next wbLoop
ErrorMessageExit:
FindWorkbookWithoutOnErrorResumeNext = "#Could not resolve workbook name '" & sBookName & "'!"
SingleExit:
Exit Function
End Function

Related

Copy from a workbook to another workbook

I am having issues developing this code. I was able to develop the code to copy new data from my workbook to an existing path but am running into issues when trying to retrieve data from the existing path work book.
The concept is that there is a workbook in my system that will be collecting data. The data comes from different users that are working on project information. Once they have completed the project this new information along with existing information gets uploaded back to the workbook collecting that data. The work book collecting the data will always have a defined path. The workbooks that users are working off of will be in multiple places across the system.
The below macro keeps failing on the "Organizer.Sheets("Partnumber_Vendor_Database").Select". I am unsure why.
"Organizer" is the local database the user will use.
"Partnumber_Vendor_File" is the local database the information is stored.
If you can see that this code could be developed better please let me know! :)
Sub Find_Partnumber_Vendor_File()
' This sub is to open the partnumber_Vendor file to update the local database.
On Error Resume Next
Dim Organizer As Workbook
Set Organizer = Application.ActiveWorkbook
Dim Partnumber_Vendor_File As Workbook
Set Partnumber_Vendor_File = Workbooks.Open("S:\Supply Chain\PURCHASING\Forms and Templates\BOM Organizer\Partnumber_Vendor_File.xlsx")
If Err.Number = 1004 Then
MsgBox "Could not open. Check path in VBA"
Exit Sub
End If
If Partnumber_Vendor_File.ReadOnly Then
MsgBox "Sorry, partnumber to vendor database was already in use, try later"
Exit Sub
End If
On Error GoTo 0
Dim Data As Long
Data = ActiveSheet.Cells(Rows.count, 1).End(xlUp).Row
Range("A1:" & "D" & Data).Copy
Organizer.Sheets("Partnumber_Vendor_Database").Select
Range("A1:D1").Select
Selection.Insert Shift:=xlDown
Partnumber_Vendor_File.Close
End Sub
Althoug it is easy to use, avoid ActiveWorkbook, ActiveSheet and Sheets(<Title of the Sheet.).
The problem with the first two is that is hard to tell if the activeworkbook is actually the workbook you are looking for, specially when there are more than 1 workbook open. To workaround this, one solution is the use the Workbooks object to select the correct work book by its NAME (property CodeName). The Same for the Sheets, change the REAL NAMES of the Sheets so you can properlly call them.
The third is basically the same principle. In general, do not use titles of Sheets as references, use the REAL NAME of the object. Use the Property window in the VBA code to change that.
The error may come from 2 situations:
1 - Your selected workbook is not the actual workbook you want to work on.
2 - The Sheet "Partnumber_Vendor_Database" had its title changed or miswritten.
Hope it helps.

I can't figure out why this is giving me a runtime error 9 (yes, the files are present and loaded)

Sub CopyColumnWidths(FileName1, SheetName1, FileName2, SheetName2)
ColumnNumber = 1
Check = WorksheetFunction.CountA(Workbooks(FileName1).Sheets(SheetName1).Columns(ColumnNumber))
Do While Check > 0
ColumnLetter = LastColumnLetter(ColumnNumber)
Workbooks(FileName2).Sheets(SheetName2).Columns(ColumnNumber).ColumnWidth = Workbooks(FileName1).Sheets(SheetName1).Columns(ColumnNumber).ColumnWidth
ColumnNumber = ColumnNumber + 1
Check = WorksheetFunction.CountA(Workbooks(FileName1).Sheets(SheetName1).Range(ColumnLetter & ":" & ColumnLetter))
Loop
End Sub
Ok, so this is my code. I have verified that all the file names and sheet names are present and accounted for in the same instance of Excel. I have checked for misspellings, extra characters, and 'invisible' characters and none are present.
I tried, for troubleshooting purposes, putting in a Workbooks(FileName1).Activate and it wouldn't work either. In different code that particular file does get hidden, but at the time this code is executed that workbook is visible and present.
For the life of me I cannot figure out why this is breaking and could use a hand.
This is running on Excel 2013, 64-bit if it matters.
------- More Info
FileName1 is "Original Datasheet.xlsx"
FileName2 is "Split Datasheet.xlsx"
SheetName1 (and SheetName2) are "1a. Contents"
When I try to activate FileName2, it works. When I try to activate FileName1, it fails. The sheet names don't matter, it doesn't 'see' FileName1, even though it is present and I can select it in the 'Switch Windows' dropdown.
Repeating again for those who didn't read the title the first time: Yes, all files are loaded in the same instance of Excel. All Files Are Present.
I don't have your answer, but runtime error 9 sounds a lot like one of these values isn't what you think it is - and with the code you have it's hard to tell exactly where it's blowing up.
Start with turning this:
Sub CopyColumnWidths(FileName1, SheetName1, FileName2, SheetName2)
into this (assuming the procedure is called from within the same module - if it's called from another module, make it Public Sub):
Private Sub CopyColumnWidths(ByVal FileName1 As String, ByVal SheetName1 As String, ByVal FileName2 As String, ByVal SheetName2 As String)
Changing your signature to use String parameters passed by value shouldn't break your code in any way, but makes things more explicit and therefore improves readability and makes your intent clearer.
Moving on.
ColumnNumber = 1
Where's that coming from? Declare it. Stick Option Explicit at the top of your module, and then declare every variable until your code compiles again (Option Explicit will make VBA refuse to compile code that uses undeclared variables).
Dim ColumnNumber As Long
ColumnNumber = 1
Now that we know ColumnNumber and Check are local variables declared in the same scope (right?), we move on:
Dim Check As Long
Check = WorksheetFunction.CountA(Workbooks(FileName1).Sheets(SheetName1).Columns(ColumnNumber))
This line is doing too many things: we don't know that Workbooks(FileName1) is succeeding, and we don't know that its Sheets(SheetName1) is succeeding either - yet we call its Columns member regardless, assuming blue skies and sunshine.
Don't assume blue skies and sunshine.
Break it down.
Dim sourceBook As Workbook
Set sourceBook = Workbooks(FileName1)
Dim sourceSheet As Worksheet
Set sourceSheet = sourceBook.Worksheets(SheetName1)
Check = WorksheetFunction.CountA(sourceSheet.Columns(ColumnNumber))
If your code runs up to that point, your problem is half-solved - you have the same issue here, and you're fetching the same Workbook and Worksheet objects again - instead, break it down, assign local object variables, and reuse them:
Workbooks(FileName2).Sheets(SheetName2).Columns(ColumnNumber).ColumnWidth = Workbooks(FileName1).Sheets(SheetName1).Columns(ColumnNumber).ColumnWidth
Dim destinationBook As Workbook
Set destinationBook = Workbooks(FileName2)
Dim destinationSheet As Worksheet
Set destinationSheet = destinationBook(SheetName2)
destinationSheet.Columns(ColumnNumber).ColumnWidth = sourceSheet.Columns(ColumnNumber).ColumnWidth
ColumnNumber = ColumnNumber + 1
Check = WorksheetFunction.CountA(sourceSheet.Range(ColumnLetter & ":" & ColumnLetter))
Step through (F8) this code line by line, then you'll know exactly which instruction is blowing up. Cramming everything into as few code lines as possible and chaining 2, 3, 4 member accesses assuming it will "just work" makes it nearly impossible to know that.
Is it possible that previous code that hides the window for FIleName1 but later unhides it is messing things up? I had to do that because Excel kept putting data on the wrong workbook unless I hid it.
This sounds like you have code (elsewhere?) that works off ActiveSheet, or ActiveWorkbook, explicitly or (more likely?) implicitly.
If you find yourself using unqualified Range, Cells, Rows, Columns or Names calls, you're implicitly referring to the active sheet. Replace them with explicitly qualified member calls using the worksheet object you mean to refer to. There's no magic, Excel (VBA actually) can't guess your intentions; it doesn't "keep putting data on the wrong workbook" - it puts data exactly where you tell it to put it. Hiding the workbook is just a terribly bad work-around: you're still not telling it explicitly where you mean to put it.
I have been looking at this too long.
FileName1 was coming across as "Orignial Datasheet.xlsx" not "Original Datasheet.xlsx"
That is why it wasn't connecting. My mistake, sorry for bothering everyone.

VBA Error 1004 - PasteSpecial method of Range class

Recently I started getting the error 1004: PasteSpecial method of Range class failed. I know people have said before that it might be that it is trying to paste to the active sheet when there is none, however, as you can see, everything is based on ThisWorkbook so that shouldn't be the problem. It happens extra much when Excel doesn't have the focus.
'references the Microsoft Forms Object Library
Sub SetGlobals()
Set hwb = ThisWorkbook' home workbook
Set mws = hwb.Worksheets("Code Numbers") ' main worksheet
Set hws = hwb.Worksheets("Sheet3") ' home worksheet (Scratch pad)
Set sws = hwb.Worksheets("Status") ' Status sheet
Set aws = hwb.Worksheets("Addresses") ' Addresses sheet
End Sub
Sub Import()
Call SetGlobals
hws.Select
'a bunch of code to do other stuff here.
For Each itm In itms
Set mitm = itm
body = Replace(mitm.HTMLBody, "<img border=""0"" src=""http://www.simplevoicecenter.com/images/svc_st_logo.jpg"">", "")
Call Buf.SetText(body)
Call Buf.PutInClipboard
Call hws.Cells(k, 1).Select
Call hws.Cells(k, 1).PasteSpecial
For Each shape In hws.Shapes
shape.Delete
Next shape
'Some code to set the value of k
'and do a bunch of other stuff.
Next itm
End Sub
Update: mitm and itm have two different types, so I did it for intellisense and who knows what else. This code takes a list of emails and pastes them into excel so that excel parses the html (which contains tables) and pastes it directly into excel. Thus the data goes directly into the sheet and I can sort it and parse it and whatever else I want.
I guess I'm basically asking for anyone who knows another way to do this besides putting it in an html file to post it. Thanks
This probably will not exactly answer your problem - but I noticed a few things in your source code that are too long to place in a comment, so here it is. Some of it is certainly because you omitted it for the example, but I'll mention it anyway, just in case:
Use Option Explicit - this will avoid a lot of errors as it forces you to declare every variable
Call SetGlobals can be simplified to SetGlobals - same for Call Buf.SetText(body) = Bof.SetText Body, etc.
No need to '.Select' anything - your accessing everything directly through the worksheet/range/shape objects (which is best practice), so don't select (hws.Select, hws.Cells(k,1).Select)
Why Set mitm = itm? mitm will therefore be the same object as itm - so you can simply use itm
You're deleteing all shapes in hwsmultiple times - for each element in itms. However, once is enough, so move the delete loop outside of the For Each loop
Instead of putting something in the clipboard and then pasting it to a cell, just assign it directly: hws.Cells(k, 1).Value = body - this should solve your error!
Instead of using global variables for worksheets that you assign in 'SetGlobals', simply use the sheet objects provided by Excel natively: If you look at the right window in the VBE with the project tree, you see worksheet nodes Sheet1 (sheetname), Sheet2 (sheetname), etc.. You can rename these objects - go to their properties (F4) and change it to meaningful names - or your current names (hwb, mws, ...) if you want. Then you can access them throughout your code without any assignment! And it'll work later, even if you change the name of Sheet3to something meaningful! ;-)
Thus, taking it all into account, I end up with the following code, doing the same thing:
Option Explicit
Sub Import()
'a bunch of code to do other stuff here.
For Each shape In hws.Shapes
shape.Delete
Next shape
For Each itm In itms
Call hws.Cells(k, 1) = Replace(itm.HTMLBody, "<img border=""0"" src=""http://www.simplevoicecenter.com/images/svc_st_logo.jpg"">", "")
'Some code to set the value of k
'and do a bunch of other stuff.
Next itm
End Sub

Excel VBA - Code to move or copy worksheets without warnings

I am trying to create a macro that would act the same as right clicking a workbook tab, selecting move or copy, checking the copy option, selecting another open workbook and clicking ok but without the warnings. I found the code to disable warning and I was able to record a macro that does what I want but I don't know how to make it request which open workbook to copy to.
In short how do I make the following code work where WorksheetIWantToCopy is the one the user currently has selected and OpenWorkbookIWantToCopyToo.xlsx is a workbook to be selected by the user out of a list of open workbooks.
Application.DisplayAlerts = False
Sheets("**WorksheetIWantToCopy**").Select
Sheets("**WorksheetIWantToCopy**").Copy Before:=Workbooks( _
"**OpenWorkbookIWantToCopyToo.xlsx**").Sheets(1)
I appreciate any information anyone can provide. My team greatly appreciates your support (we currently have to hit ok on 25 warnings due to conflicts we don't really care about). Thx!
If the worksheet you want to copy will always be the active sheet then you can use ActiveSheet.
As for letting the user select a workbook, it can be as simple as using the InputBox.
Public Function getWorkbookName() As String
Dim i As Integer, sListOfWbks As String, sRsp As String
' build list of workbooks
For i = 1 To Workbooks.Count
sListOfWbks = sWbkList & vbCrLf & i & " - " & Workbooks(i).Name
Next i
sRsp = InputBox("Select workbook." & vbCrLf & sListOfWbks)
If IsNumeric(sRsp) Then
getWorkbookName = Workbooks(CInt(sRsp)).Name
Else
' user pressed cancel or entered invalid text
getWorkbookName = ""
End If
End Function
This basic example will of course list all workbooks, including hidden add-ins and the workbook you are moving away from.
This needs to be said before anything else: always, always, ALWAYS make use of .Copy instead of .Move when automatically shuffling excel workbooks with VBA. Move has inherent risks because it is a modification of the other file, and if your code misbehaves then you could lose all of the data you're working with.
First of all, know which workbook is which, with no ambiguity:
Dim wkbkDestination, wkbkTemporary As Workbook
Set wkbkDestination = Workbooks("OpenWorkbookIWantToCopyTo.xlsx")
Set wkbkTemporary = Workbooks.Open("WorkbookIWantToCopy.xlsx")
Next, Copy your desired tab to your destination workbook, rename the new tab to prevent errors, and close the second workbook, without saving.
wkbkTemporary.Worksheets("WorksheetIWantToCopy").Copy Before:=wkbkDestination.Worksheets(1)
wkbkDestination.Worksheets(1).Name = "WorkbookIWantToCopy"
wkbkTemporary.Close SaveChanges = False
Naturally, depending on the exact controls you intend to use, there are lots of ways this code could be implemented. From your description it is somewhat unclear what exact problem you're trying to solve and whether this is a one-off event you're trying to accomplish for a given list of files, or whether this function is to be used on an ongoing basis.

Cancel External Query in Excel VBA

I have created an Excel Spreadsheet which helps with data analysis from an Oracle database.
The user enters then clicks the "Refresh Query" button which generates a query for Oracle to execute. The query takes a minute or so to complete. Although the VBA code does not hang on ".Refresh", all Excel windows remain frozen until the query completes.
Sub refreshQuery_click()
Dim queryStr as String
' Validate parameters and generate query
' ** Code not included **
'
' Refresh Query
With ActiveWorkbook.Connections("Connection").OLEDBConnection
.CommandText = queryStr
.Refresh
End With
End Sub
Is there a way for the user to manually cancel the query (calling .CancelRefresh) while the Excel user-interface is frozen?
EDIT I don't know if the following is worth noting or regular behavior. While the query is executing, all open Excel windows (including the VBA Editor) become "Not Responding" in Task Manager. Neither pressing Esc nor Ctrl+Break will cancel the script. Also, calling DoEvents (either before or after .Refresh) does not change this behavior.
Here's a method that I know will work. However, there are some complications.
Here's how it's done:
Put the spreadsheet with the data in a separate workbook. This worksheet should execute the refresh query when it's opened and then close once the data is updated.
Create a batch file to call the "Data" Excel file.
Within a different workbook, create a procedure (macro) for the user to call. This procedure will call the batch file, which subsequently calls the Excel file. Since you are calling a batch file and not Excel directly, the Excel procedure will continue because the command shell is released so quickly and opens the other Excel file in a different thread. This allows you to continue working within the main Excel file.
Here are some complications:
I included a method to alert the user that the data has been udpated. There are timing issues where it's possible to try to check if the data has been update when the workbook is not accessible, which forces the user to try to update values. I included a method called my time which pauses the execution of the code so it only checks every so many seconds.
The updated worksheet will pop up in a new window, so the user will need to click on their original worksheet and keep working. You could learn to hide this if you're comfortable with Windows scripting (I haven't learned that yet).
Here are some files and code. Be sure to read the comments in the code for why some things are there.
FILE: C:\DataUpdate.xls
We'll make a workbook called "DataUpdate.xls" and put it in our C:\ folder. In cell A1 of Sheet1, we'll add our QueryTable which grabs external data.
Option Explicit
Sub UpdateTable()
Dim ws As Worksheet
Dim qt As QueryTable
Set ws = Worksheets("Sheet1")
Set qt = ws.Range("A1").QueryTable
qt.Refresh BackgroundQuery:=False
End Sub
Sub OnWorkbookOpen()
Dim wb As Workbook
Set wb = ActiveWorkbook
'I put this If statement in so I can change the file's
'name and then edit the file without code
'running. You may find a better way to do this.
If ActiveWorkbook.Name = "DataUpdate.xls" Then
UpdateTable
'I update a cell in a different sheet once the update is completed.
'I'll check this cell from the "user workbook" to see when the data's been updated.
Sheets("Sheet2").Range("A1").Value = "Update Table Completed " & Now()
wb.Save
Application.Quit
End If
End Sub
In the ThisWorkbook object in Excel, there's a procedure called Workbook_Open(). It should look like the following so it executes the update code when it is opened.
Private Sub Workbook_Open()
OnWorkbookOpen
End Sub
NOTE: I found a bug when this file closed if 1) you accessed the file from the command line or shell and 2) you have the Office Live Add-in installed. If you have the Office Live Add-in installed, it will throw an exception on exit.
FILE: C:\RunExcel.bat
Next, we're going to create a batch file that will open the Excel file we just made. The reason that call the Excel file from within the batch file and not directly from the other Excel file using Shell is because Shell will not continue until the other application closes (at least when using Excel.exe "c:\File.xls"). The batch file, however, runs its code and then immediately closes, thus allowing the original code that called it to continue. This is what will let your uses continue working in Excel.
All this file needs is:
cd "C:\Program Files\Microsoft Office\Office10\"
Excel.exe "C:\DataUpdate.xls"
If you're handy with Windows Scripting, you do fancy things like open the window in a hidden mode or pass a parameter of the file name or Excel location. I kept it simple with a batch file.
FILE: C:\UserWorkbook.xls
This is the file that the user will open to "do their work in." They'll call the code to update the other workbook from within this workbook and they'll still be able to work in this workbook while this one is updating.
You need a cell in this workbook where you'll check the "Update Table Completed" cell from the DataUpdate workbook. I chose cell G1 in Sheet1 for my example.
Add the following code to a VBA module in this workbook:
Option Explicit
Sub UpdateOtherWorkbook()
Dim strFilePath As String
Dim intOpenMode As Integer
Dim strCallPath As String
Dim strCellValue As String
Dim strCellFormula As String
Dim ws As Worksheet
Dim rng As Range
Set ws = Worksheets("Sheet1")
Set rng = ws.Range("G1")
strCellFormula = "='C:\[DataUpdate.xls]Sheet2'!A1"
'This makes sure the formula has the most recent "Updated" value
'from the data file.
rng.Formula = strCellFormula
strFilePath = "C:\RunExcel.bat"
intOpenMode = vbHide
'This will call the batch file that calls the Excel file.
'Since the batch file executes it's code and then closes,
'the Excel file will be able to keep running.
Shell strFilePath, intOpenMode
'This method, defined below, will alert the user with a
'message box once the update is complete. We know that
'the update is complete because the "Updated" value will
'have changed in the Data workbook.
AlertWhenChanged
End Sub
'
Sub AlertWhenChanged()
Dim strCellValue As String
Dim strUpdatedCellValue As String
Dim strCellFormula As String
Dim ws As Worksheet
Dim rng As Range
Set ws = Worksheets("Sheet1")
Set rng = ws.Range("G1")
strCellFormula = "='C:\[DataUpdate.xls]Sheet2'!A1"
strCellValue = rng.Value
strUpdatedCellValue = strCellValue
'This will check every 4 seconds to see if the Update value of the
'Data workbook has been changed. MyWait is included to make sure
'we don't try to access the Data file while it is inaccessible.
'During this entire process, the user is still able to work.
Do While strCellValue = strUpdatedCellValue
MyWait 2
rng.Formula = strCellFormula
MyWait 2
strUpdatedCellValue = rng.Value
DoEvents
Loop
MsgBox "Data Has Been Updated!"
End Sub
'
Sub MyWait(lngSeconds As Long)
Dim dtmNewTime As Date
dtmNewTime = DateAdd("s", lngSeconds, Now)
Do While Now < dtmNewTime
DoEvents
Loop
End Sub
As you can see, I constantly updated the formula in the "Listening Cell" to see when the other cell was updated. Once the data workbook has been updated, I'm not sure how you'd force an update in code without rewriting all the cells. Closing the workbook and reopening it should refresh the values, but I'm not sure of the best way to do it in code.
This whole process works because you're using a batch file to call Excel into a different thread from the original file. This allows you to work in the original file and still be alerted when the other file has been updated.
Good luck!
EDIT: Rather than include a more complete answer in this same answer, I've created a separate answer dedicated entirely to that solution. Check it out below (or above if it gets voted up)
Your users can break the VBA function by pressing Ctrl+Break on the keyboard. However, I've found that this can cause your functions to randomly break until each time any function is run. It goes away when the computer is restarted.
If you open this file in a new instance of Excel (meaning, go to Start > Programs and open Excel from there), I think that the only workbook that will be frozen will be the one executing the code. Other intances of Excel shouldn't be affected.
Lastly, you might research the DoEvents functions, which yields execution back to the Operating System so that it can process other events. I'm not sure if it would work in your case, but you could look into it. That way you can do other things while the process is being completed (It's kind of dangerous because the user can then change the state of your application while the process is working).
I believe I know a way that actually will work, but it's complicated and I don't have the code in front of me. It involves creating a separate instance of the Excel application in code and attaching a handler to the execution of that instance. You include the DoEvents part of the code in a loop that releases once the application closes. The other instantiated Excel application has the sole purpose of opening a file to execute a script and then close itself. I've done something like this before so I know that it works. I'll see if I can find the code tomorrow and add it.
Well, you could consider the old-fashion way -- split the query into smaller batches and use Do Events in between batches.
You could try XLLoop. This lets you write excel functions (UDfs) on an external server. It includes server implementations in many languages (eg. Java, Ruby, Python, PHP).
You could then connect to your oracle database (and potentially add a caching layer) and serve up the data to your spreadsheet that way.
The XLL also has a feature to popup a "busy" GUI that lets the user cancel the function call (which disconnects from the server).
BTW, I work on the project so let me know if you have any questions.