6
\$\begingroup\$

I have the following but it takes long time to run, it is a simple wherein the user select a file and data from Sheet1 is copied to another workbook.

Sub ImportApp(ByVal filepath_Report As String, file_name1 As String, wsOutput As Worksheet)
 Application.ScreenUpdating = False
 Set wbReport = Workbooks.Open(file_name1)
 Set wsReport = wbReport.Sheets(1)
 Select Case wsOutput.Name
 Case Is = "Downtilt Tracker"
 wsReport.Activate
 With Rows(1)
 Set d = .Find("Completed Date")
 If d Is Nothing Then
 MsgBox "Invalid Down-Tilt Tracker." & vbNewLine & "Please import a valid Down-Tilt Tracker."
 wbReport.Close False
 Sheets("Control").Activate
 Application.ScreenUpdating = True
 Exit Sub
 End If
 End With
 End Select
 wsReport.Activate: wsReport.Cells.Copy: wsOutput.Activate: Cells(1, 1).Select: ActiveSheet.Paste: Cells.EntireColumn.AutoFit: wsOutput.Range("A1:AB" & 1048576).HorizontalAlignment = xlCenter
 wsOutput.Cells(1, 1).Select
 Application.CutCopyMode = False
 wbReport.Close False
End Sub
200_success
145k22 gold badges190 silver badges478 bronze badges
asked Mar 30, 2015 at 22:35
\$\endgroup\$
9
  • \$\begingroup\$ Define "a long time". 10 seconds? A few minutes? How large are the files that you're opening? \$\endgroup\$ Commented Mar 30, 2015 at 23:42
  • \$\begingroup\$ 3 Min, the file size is about 3Mbps with formatting \$\endgroup\$ Commented Mar 30, 2015 at 23:45
  • \$\begingroup\$ Just by chance, would the source workbook happen to be stored on a network drive? \$\endgroup\$ Commented Mar 30, 2015 at 23:47
  • \$\begingroup\$ no on local drive. \$\endgroup\$ Commented Mar 30, 2015 at 23:48
  • \$\begingroup\$ Linked data sources then? Lots of formulas? I find it difficult to believe this code is running that long. Step through the code line by line and see which one seems to "freeze" it for a while. \$\endgroup\$ Commented Mar 30, 2015 at 23:55

2 Answers 2

3
\$\begingroup\$
  • First things first - your code has some readability issues due to the use of the colon operator:

    wsReport.Activate: wsReport.Cells.Copy: wsOutput.Activate: Cells(1, 1).Select: ActiveSheet.Paste: Cells.EntireColumn.AutoFit: wsOutput.Range("A1:AB" & 1048576).HorizontalAlignment = xlCenter
    

    This makes it incredibly difficult to tell at a glance what the code is doing - reading down in a lot easier than reading left to right when you are trying to work through a sequence. This is much, much better:

    wsReport.Activate
    wsReport.Cells.Copy
    wsOutput.Activate
    Cells(1, 1).Select
    ActiveSheet.Paste
    Cells.EntireColumn.AutoFit
    wsOutput.Range("A1:AB" & 1048576).HorizontalAlignment = xlCenter
    
  • Second, you have an unused variable in the Sub declaration - filepath_Report is never used. I'd remove it.

  • Speaking of your Sub declaration, it looks like the intent is to pass all of the parameters by value, but in fact only the first parameter is passed by value - ByRef is the default and each parameter needs the ByVal modifier to override the default. The code below demonstrates:

    Private Sub ByValDemo()
     Dim one As String, two As String
     one = "Variable one set in ByValDemo"
     two = "Variable two set in ByValDemo"
     Call IsThisByVal(one, two)
     Debug.Print one
     Debug.Print two
    End Sub
    Private Sub IsThisByVal(ByVal one As String, two As String)
     one = "Variable one set in IsThisByVal"
     two = "Variable two set in IsThisByVal"
    End Sub 
    

    Debug output of running ByValDemo is:

    Variable one set in ByValDemo
    Variable two set in IsThisByVal

    To declare both strings as ByVal (the Worksheet has to be ByRef), you need to do this:

    Sub ImportApp(ByVal filepath_Report As String, ByVal file_name1 As String, wsOutput As Worksheet)
    
  • The Select...Case structure is confusing as you only have one Case. A simple If...Then would suffice.

  • Although it probably isn't effecting anything, turning off screen updates and then only turning it back on if your conditional executes probably isn't the best way to handle it. Excel should turn it back on for you when the Sub exits, but I wouldn't rely on that.

  • Per your comment, it seems like you are under the impression that you aren't using the Windows Clipboard. This isn't accurate. The call to wsReport.Cells.Copy puts the entire Worksheet on the clipboard. The .Cells property contains every possible cell in the worksheet (even unused). You can confirm this by running the following, opening Notepad and hitting Ctrl-V:

    Private Sub CanIHazClipboard()
     Dim sheet As Worksheet
     Set sheet = Application.ActiveSheet
     sheet.Cells.Copy
     'Prints 16384, regardless of what is being used.
     Debug.Print sheet.Cells.Columns.count
     'This would actually overflow the return variable...
     'Debug.Print sheet.Cells.Rows.Count
    End Sub
    
  • The Application methods such as .Select, .Activate, .Copy, and .Paste functions are really slow compared to using the objects that you already got references to. Use the Range properties instead:

    Dim target As Range
    'Set the "paste" range to the same cells as the source (needs to be the same size).
    Set target = wsOutput.Range(wsReport.UsedRange.Address)
    'If you need formulas...
    target.Formula = wsReport.UsedRange.Formula
    '...or if you only need to copy values.
    target.Value2 = wsReport.UsedRange.Value2
    

    This is not only incredibly faster, it doesn't trash the clipboard and you don't need to keep track of what is "activated".

  • You have an Exit Sub buried deep in a nested structure. My personal rule is that if I'm going to bail out of a routine early, that shouldn't happen at more than one level of indentation. I would personally structure the flow control in this case so that it always reaches the bottom of the Sub so you Don't Repeat Yourself with your clean-up code (I also try to avoid the .Find method):

    Dim found As Boolean, cell As Range
    If Not wsOutput.Name = "Downtilt Tracker" Then
     'I would assume this should also be an error condition...
    Else
     For Each cell In wsOutput.UsedRange.Rows(1).Cells
     If cell.Value2 = "Completed Date" Then
     found = True
     Exit For
     End If
     Next cell
     If found Then
     'Do your copy
     Else
     'Signal your error condition.
     MsgBox "Invalid Down-Tilt Tracker." & vbNewLine & _
     "Please import a valid Down-Tilt Tracker."
     End If
    End If
    'Only need the clean-up code once.
    Application.ScreenUpdating = True
    wbReport.Close False
    
answered Mar 31, 2015 at 1:46
\$\endgroup\$
4
\$\begingroup\$

I like your indentation. It's nice to see well-indented VBA code, it needs to be pointed out!

Application.ScreenUpdating = False

That's generally a good idea, but all by itself, it makes a poor UX - and if calculation mode remains xlAutomatic, it might be a missed opportunity to gain some more performance by turning off calculations while you're working on the worksheet.

Thing is, that's a whole concern all by itself, and as such it deserves its own function. Almost all my macro-enabled Excel workbooks have this function somewhere:

Private Sub ToggleWaitMode(Optional ByVal wait As Boolean = True)
 With Excel.Application
 .Calculation = IIf(wait, xlCalculationManual, xlCalculationAutomatic)
 .Cursor = IIf(wait, xlWait, xlDefault)
 .StatusBar = IIf(wait, "Please wait...", False)
 .DisplayAlerts = Not wait
 .ScreenUpdating = Not wait
 End With
End Sub

So instead of Application.ScreenUpdating = False, you can say ToggleWaitMode, and then do ToggleWaitMode False when you're done processing.

The problem with turning ScreenUpdating off, is that if anything goes wrong, it stays off and the user (and sometimes even the dev) is left thinking Excel is frozen: whenever you play with ScreenUpdating, you must handle runtime errors:

Private Sub DoSomething()
 On Error GoTo CleanFail
 'do something
 CleanExit:
 'clean up code
 ToggleWaitMode False
 Exit Sub
 CleanFail:
 'error-handling code
 Resume CleanExit
End Sub

This way you're always sure that all exit paths involve resetting ScreenUpdating and calculation mode.

...which is a good idea, because the next line can actually blow up whenever a bad file_name1 is passed to the procedure:

Set wbReport = Workbooks.Open(file_name1)

Set wsReport = wbReport.Sheets(1)

I like that: by assigning an object reference, you can work against that object. There are a few issues:

  • Where's the declaration? Always use Option Explicit and declare all variables! If the variable is declared at module level, then the declaration belongs inside the procedure's scope - move it there, to the smallest possible scope.
  • The Sheets collection contains chart sheets and actual worksheets. You probably intend to query the Worksheets collection here; if you had declared the wsReport variable As Worksheet and the Sheets(1) object were actually a chart sheet, you'd have a runtime error here.
Select Case wsOutput.Name
 Case Is = "Downtilt Tracker"

Several things here:

  • You're clearly using the wrong construct here - this should definitely be an If...Then block; what's wrong with this?

    If wsOutput.Name = "Downtilt Tracker" Then
     '...
    End If
    
  • I'm actually surprised Case Is = "string literal" actually compiles and works as intended; it's a pretty convoluted way of doing Case "string literal"...


And then things get a bit out of hand:

 wsReport.Activate
 With Rows(1)
 Set d = .Find("Completed Date")
 If d Is Nothing Then
 MsgBox "Invalid Down-Tilt Tracker." & vbNewLine & "Please import a valid Down-Tilt Tracker."
 wbReport.Close False
 Sheets("Control").Activate
 Application.ScreenUpdating = True
 Exit Sub
 End If
 End With
  • You have a reference to the wsReport object - you don't need to .Activate it.
  • The With block adds unnecessary indentation and confusion. I'd do wsReport.Rows(1).Find instead.
  • d is a meaningless name that doesn't tell anything about what you're doing here.
  • You need a reference to the "Control" sheet. Declare and assign another Worksheet variable, and use it instead of Selecting and Activateing and using implicit-context Cells.
  • If d isn't Nothing, you don't appear to be setting ScreenUpdating back to True. The control flow described in my introduction would solve that.

Avoid this - AT ALL COSTS:

wsReport.Activate: wsReport.Cells.Copy: wsOutput.Activate: Cells(1, 1).Select: ActiveSheet.Paste: Cells.EntireColumn.AutoFit: wsOutput.Range("A1:AB" & 1048576).HorizontalAlignment = xlCenter

There are 7 instructions on that line of code; there's no reason to do this. Ever.

And here's where I'd think your bottleneck is:

wsReport.Cells.Copy

You're copying the entire worksheet.

wsOutput.Range("A1:AB" & 1048576)

Why bother with the concatenation here? It's hard-coded anyway, and besides, A1 has its row specified inside the string literal.

wsOutput.Range("A1:AB1048576")

This might work a bit better - if not, it's at least certainly much easier to read:

wsReport.UsedRange.Copy
wsOutput.Range("A1").Paste
wsOutput.UsedRange.Columns.EntireColumn.AutoFit
wsOutput.UsedRange.HoriontalAlignment = xlCenter
answered Mar 31, 2015 at 1:25
\$\endgroup\$
3
  • 1
    \$\begingroup\$ I totally missed the Is = "String" syntax. I actually had to test it to see if it worked (which it does). Then I remembered that Is really isn't an operator like VB.NET - it's freaky VBA syntax for boolean Case conditionals. VBA inserts it automatically if you put an = after Case. I'm guessing it's to support the equally weird Select Case True syntax (that I shamefully admit to using sometimes). \$\endgroup\$ Commented Mar 31, 2015 at 2:14
  • \$\begingroup\$ It blew my mind too @Comintern. The examples I saw when I looked it up used it to find ranges (that would be better as ifs IMO). Case Is < 100 and such. \$\endgroup\$ Commented Mar 31, 2015 at 8:49
  • \$\begingroup\$ Thanks a lot I was successfully able to bring down the run time from 3 mins to 1 min... \$\endgroup\$ Commented Mar 31, 2015 at 15:50

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.