Wednesday, November 16, 2011

Excel VBA: Display the Developer Toolbar or Ribbon

By default the developer toolbar or ribbon (depending on the Excel version) is hidden in Microsoft Excel. In Excel 2007 and later it can be quite tricky to figure out where to enable the developer ribbon if you don't know where to look. To make things even worse, in Excel 2010 they have changed the way of enabling the ribbon.

Excel 2010
In Excel 2010 you can display the developer toolbar the following way:
  1. Click the green File Button
  2. Press Options
  3. Make sure that the Customize Ribbon right menu item is selected.
  4. In the dropdown list called Customize the Ribbon, select All Tabs.
  5. In the group called Main Tabs, make sure that the option Developer is checked.

Excel 2007
To display the developer ribbon, do the following:
  1. Click the Office Button
  2. Click the Excel Options button at the bottom of the dialog.
  3. Ensure that the Popular tab in the left menu is selected (se picture below)
  4. Check the option Show Developer tab in the Ribbon

Excel 2003
In older versions of excel the developer ribbon is divided between two toolbars: Control Toolbox and Forms. The toolbars can be displayed the following way:
  1. Press View on the main toolbar
  2. Select Toolbars
  3. Check the toolbar Control Toolbox
  4. Check the toolbar Forms

Monday, November 7, 2011

Visual Studio 2010: Debug the .NET assemblies

The .NET framework is under heavy development and bugs are removed by Microsoft developers when found. However, you may still experience major bugs that might cause your application to crash. Sometimes it is even hard to determine whether it is a bug in your own code or in the framework. This issue has been taken to a new level in Visual Studio 2010. A feature, which by default is disabled in Visual Studio, enables you to debug the entire .NET source code directly within VS2010. Previously tools like .NET Reflector and ILSpy were required to identify any bugs in the .NET framework.

To enable .NET framework code debugging, please do the following:

1) In Visual Studio 2010, select Debug -> Options and Settings...
The Options dialog is displayed.

2) Select Debugging in the left menu (se picture below) and check the option Enable .NET Framework source stepping.

When running your application in debug mode, Visual Studio will download the .NET source code when needed. Please be aware that the source code might use a lot of free space on your hard drive depending on how many .NET assemblies that are in use.

Blogger: Pretty formatting source code

When writing a blog related to programming it is quite tricky and troublesome to format the source code in a readable way. That is why we need pretty-formating tools. The following sites offer free tools for formatting source code:

  • C# Format - despite the name, this online tools allows you to pretty-format C#, VB, HTML and T-SQL. The source code in .NET is even available.

Excel VBA: Adding custom Button to the Toolbar or Ribbon

Custom buttons are added to the toolbars or ribbons when a Microsoft Excel sheet is opened, and removed when the sheet is closed. To be notified when a given Excel sheet is loaded we need to listen to the Workbook_Open event of the ThisWorkbook object found in the VBA Project explorer.
Please notice that the following code works best with Excel versions prior to Office 2007. If used in newer versions, from Excel 2007, the button and commandbar will be added to the "Add-Ins" tab. It is only possible to add buttons to the main ribbon using dynamic XML when using VBA. Se below for more information.

' ConstantsPrivate Const COMMANDBAR_NAME As String = "Custom Toolbar"Private Const BUTTON_CAPTION As String = "My Button"' OpenPrivate Sub Workbook_Open()
    ' Variables
    Dim objCommandBar As CommandBar
    Dim objButton As CommandBarButton

    ' Try to get the Commandbar (if it exists)
    On Error Resume Next
    Set objCommandBar = Me.CommandBars(COMMANDBAR_NAME)
    On Error GoTo 0
    ' Was the commandbar available?
    If (objCommandBar Is Nothing) Then
        ' Create the commandbar
        On Error Resume Next
        Set objCommandBar = Application.CommandBars.Add(Name:=COMMANDBAR_NAME, Position:=msoBarTop, Temporary:=True)
        On Error GoTo 0
        ' Valid commandbar?
        If (Not objCommandBar Is Nothing) Then
            ' Add the buttons to the command bar
            With objCommandBar
                ' Add button
                Set objButton = objCommandBar.Controls.Add(Type:=msoControlButton, Temporary:=True)
                 ' Set the button properties
                With objButton
                    .Style = msoButtonIconAndCaption
                    .Caption = BUTTON_CAPTION
                    .FaceId = 258
                    .TooltipText = "Do Something"
                    .OnAction = "'" & ThisWorkbook.Name & "'!OnDoSomething"
                End With
                ' Show the command bar
                .Visible = True
            End With
        End If
    End IfEnd Sub' Before ClosePrivate Sub Workbook_BeforeClose(Cancel As Boolean)
    On Error Resume Next
    ' Try to remove the iTrade command bar
    Call Application.CommandBars(COMMANDBAR_NAME).Delete
    ' Restore error handling
    On Error GoTo 0
End Sub

The custom event called OnDoSomething must be defined in a global Module. It doesn't work to define the method in the Workbook class. Insert a new Module and add the following code:

Option Explicit

Public Sub OnDoSomething()
    MsgBox "Hello World!"End Sub

You can change the button icon by specifying another FaceId value. To get a list of all available FaceIds on you computer, download and and install the FaceID Browser:

Excel 2007 and later
To dynamically add buttons to the Ribbon you must use a combination of XML and VBA. For more information, please visit:

Other usefull pages:

Excel VBA: Adding custom buttons to the Right-Click popup menu

In Microsoft Excel it is possible to extend the right click popup menu by using VBA. The right click menu is implemented as a special CommandBar (like the toolbars in Excel 2003) with the name "Cell".
Customs buttons are added before the popup menu is displayed. It is up to your self to make sure that any custom buttons are removed when they are no longer in use. Otherwise you would end up adding a new button every time the right menu is about to be displayed.

Option Explicit

' Constants
Private Const BUTTON_CAPTION As String = "My Button"

' Before Right Click
Private Sub Workbook_SheetBeforeRightClick(ByVal objSheet As Object, ByVal Target As Range, Cancel As Boolean)
    Dim objButton As CommandBarButton
    ' Remove the e-mail popup menu
    On Error Resume Next
    With Application.CommandBars("Cell")
        Call .Controls(BUTTON_CAPTION).Delete
    End With
    On Error GoTo 0
    ' Is the sheet1?
    If (objSheet Is Sheet1) Then
        ' Add the special button
        Set objButton = Application.CommandBars("Cell").Controls.Add(Type:=msoControlButton, Temporary:=True, Before:=1)
        ' Set the button properties
        With objButton
            .Style = msoButtonIconAndCaption
            .Caption = BUTTON_CAPTION
            .FaceId = 258
            .TooltipText = "Do Something"
            .OnAction = "'" & ThisWorkbook.Name & "'!OnDoSomething"
        End With
    End If
End Sub

Notice that the method SheetBeforeRightClick is overriden for the Workbook (ThisWorkbook) object, and not the worksheet, even though it is displayed on the worksheet. We are doing a check in the VBA code before the right click menu is displayed to ensure that it is only displayed for the worksheet named "Sheet1".

Please notice that all actions, i.e. OnAction events, must be stored in a public module. It is not possible to define the OnDoSomething method in the workbook or the worksheet classes. Add a new Module (Module1) and add the following code:

Option Explicit

Public Sub DoSomething()
    MsgBox "Hello World!"
End Sub

To specify an icon for the new button, a numeric value must be set for the FaceId property of the button. There is no built-in way of displaying all possible FaceIds. However, you can download and install the Excel add-in called Face ID Browser to display all possible button icons:

 The final result will display the following custom menu item on the right click menu:


For more information on how to add buttons to CommandBars, please check out:
Tuesday, November 1, 2011

Excel VBA: String Format

The functions string.Format or StringBuilder.AppendFormat are two very usefull functions for formatting strings and increasing the readability of your .NET code. The Format function in VBA unfortunately works in a quite different way than the string.Format function in .NET. As far as I know there is no built-in function in VBA to acomplish the exact same as string.Format. In VBA the functionality can be achieved the following way:

' Format string using the .NET way
Public Function StringFormat(ByVal strValue As String, ParamArray arrParames() As Variant) As String
    Dim i As Integer

    ' Replace parameters  
    For i = LBound(arrParames()) To UBound(arrParames())
        strValue = Replace(strValue, "{" & CStr(i) & "}", CStr(arrParames(i)))
    ' Get the value    StringFormat = strValue
End Function

Excel VBA: Download files from the Internet

There is no built-in function in Microsoft Excel which allows you to download contents from the Internet on the fly. To accomplish this task we need to use the API for WinInet. The use and explanation of API in VBA is for advanced users which have prior experience from either Visual Basic 6.0 or .NET.

It is very important that all open Internet connections are closed as soon as the task is completed. WinInet only allows 2 concurrent connections to a given host. If you forget to shut down the connection after use, you will experience timeouts and misleading error messages. Please refer to the following website for more information related to the maximum allowed concurrent web connections:

The source code below should be pasted in a "Class Module" in Excel. If you are not sure how to open the VBA editor in Excel for your current Microsoft Office version, please refer to the following page:
  • Display the developer toolbar or ribbon in Excel

Create new class module:
  1. Open the Microsoft Visual Basic for Applications editor in Excel.
  2. Select Insert -> Class Module on the main menubar
  3. Rename the new class module to "WebClient"

To use the code, you shold create a new instance of the class and any of the public methods:
  • DownloadFile - download a specific resource to a local file
  • UrlExists - check if a given URL exists

Dim objClient As New WebClient
Call objClient.DownloadFile("", "c:\test.html")

The function "ReThrowError" is defined here:

Source Code

Private Declare Function InternetOpen Lib "wininet.dll" Alias "InternetOpenA" (ByVal sAgent As String, ByVal lAccessType As Long, ByVal sProxyName As String, ByVal sProxyBypass As String, ByVal lFlags As Long) As Long
Private Declare Function InternetCloseHandle Lib "wininet.dll" (ByVal hInet As Long) As Integer
Private Declare Function InternetReadFile Lib "wininet.dll" (ByVal hFile As Long, ByVal sBuffer As String, ByVal lNumBytesToRead As Long, lNumberOfBytesRead As Long) As Integer
Private Declare Function InternetConnect Lib "wininet.dll" Alias "InternetConnectA" (ByVal hInternetSession As Long, ByVal sServerName As String, ByVal nServerPort As Integer, ByVal sUsername As String, ByVal sPassword As String, ByVal lService As Long, ByVal lFlags As Long, ByVal lContext As Long) As Long
Private Declare Function InternetOpenUrl Lib "wininet.dll" Alias "InternetOpenUrlA" (ByVal hInternetSession As Long, ByVal lpszUrl As String, ByVal lpszHeaders As String, ByVal dwHeadersLength As Long, ByVal dwFlags As Long, ByVal dwContext As Long) As Long
Private Declare Function InternetGetLastResponseInfo Lib "wininet.dll" Alias "InternetGetLastResponseInfoA" (lpdwError As Long, ByVal lpszBuffer As String, lpdwBufferLength As Long) As Boolean

Private Enum EHttpQueryInfoLevel
    http_QUERY_EXPIRES = 10
    http_QUERY_PRAGMA = 17
    http_QUERY_VERSION = 18
    http_QUERY_STATUS_CODE = 19
    http_QUERY_STATUS_TEXT = 20
    http_QUERY_RAW_HEADERS = 21
    http_QUERY_FORWARDED = 30
    http_QUERY_SERVER = 37
    http_QUERY_USER_AGENT = 39
    http_QUERY_SET_COOKIE = 43
    http_STATUS_DENIED = 401
    http_STATUS_PROXY_AUTH_REQ = 407
End Enum

Private Declare Function HttpQueryInfo Lib "wininet.dll" Alias "HttpQueryInfoA" (ByVal hhttpRequest As Long, ByVal lInfoLevel As Long, ByRef sBuffer As Any, ByRef lBufferLength As Long, ByRef lIndex As Long) As Integer
Private Declare Function HttpOpenRequest Lib "wininet.dll" Alias "HttpOpenRequestA" (ByVal hHttpSession As Long, ByVal sVerb As String, ByVal sObjectName As String, ByVal sVersion As String, ByVal sReferer As String, ByVal something As Long, ByVal lFlags As Long, ByVal lContext As Long) As Long
Private Declare Function HttpSendRequest Lib "wininet.dll" Alias "HttpSendRequestA" (ByVal hhttpRequest As Long, ByVal sHeaders As String, ByVal lHeadersLength As Long, sOptional As Any, ByVal lOptionalLength As Long) As Integer

' Constants
Private Const INTERNET_FLAG_NO_CACHE_WRITE As Long = &H4000000
Private Const INTERNET_FLAG_NO_UI As Long = &H200
Private Const INTERNET_FLAG_EXISTING_CONNECT As Long = &H20000000
Private Const INTERNET_FLAG_RELOAD As Long = &H80000000


' User Agent
Private Const USER_AGENT = "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)"

' Open
Private Function OpenSession()
    Dim hSession As Long

    ' Open internet connection
    hSession = InternetOpen(USER_AGENT, INTERNET_OPEN_TYPE_PRECONFIG, vbNullString, vbNullString, 0)

    ' Valid session?
    If (hSession = 0) Then
        ' Error
        Err.Raise 1234, , "Unable to open internet connection!"
        ' Finished
        Exit Function
    End If
    ' Get the value
    OpenSession = hSession
End Function

' Close Handle
Private Sub CloseHandle(ByRef hHandle As Long)
   ' Valid handle?
   If (hHandle <> 0) Then
        ' Close
        Call InternetCloseHandle(hHandle)
        ' Clear handle
        hHandle = 0
    End If
End Sub

' Open Url
Private Function OpenUrl(ByVal hSession As Long, ByVal strUrl As String, Optional ByVal bRaiseError = True) As Long
    Dim hConnection As Long
    ' Valid session?
    If (hSession = 0) Then
        Err.Raise 2345345, , "The session is not set!"
        Exit Function
    End If
    ' Open Url
    hConnection = InternetOpenUrl(hSession, strUrl, vbNullString, ByVal 0&, INTERNET_FLAG_EXISTING_CONNECT Or INTERNET_FLAG_RELOAD, ByVal 0&)

     ' Valid file?
    If (hConnection = 0) Then
        ' Error
        Call RaiseLastError
        ' Exit
        Exit Function
    End If

    ' Get the value
    OpenUrl = hConnection

End Function

' Raise Last Error
Private Sub RaiseLastError()
    Dim strErrorMessage As String
    Dim lngErrorNumber As Long

    ' Get the last error
    lngErrorNumber = Err.LastDllError
    ' Valid error?
    If (lngErrorNumber <> 0) Then
        ' Error
        Err.Raise lngErrorNumber, , "DLL Error: " & CStr(lngErrorNumber)
        ' Get the error
        If (GetLastResponseInfo(lngErrorNumber, strErrorMessage)) Then
            ' Raise error
            Err.Raise lngErrorNumber, , strErrorMessage
        End If
    End If
End Sub

' Get Last Response Info
Private Function GetLastResponseInfo(ByRef lngErrorNumber As Long, ByRef strErrorMessage As String) As Boolean
    Dim intResult As Integer
    Dim lngBufferLength As Long
    ' Get the required buffer size
    intResult = InternetGetLastResponseInfo(lngErrorNumber, strErrorMessage, lngBufferLength)
    ' Valid length?
    If (lngErrorNumber <> 0) Then
        ' Allcoate the buffer
        strErrorMessage = String(lngBufferLength, 0)
        ' Retrieve the last respons info
        intResult = InternetGetLastResponseInfo(lngErrorNumber, strErrorMessage, lngBufferLength)
        ' Get the error message
        GetLastResponseInfo = True
        Exit Function
    End If
    ' Not an error
    GetLastResponseInfo = False
End Function

' File Exists?
Public Function UrlExists(ByVal strUrl As String) As Boolean
    On Error GoTo ErrorHandler
    Const BUFFER_LENGTH As Long = 255
    Dim hSession As Long
    Dim hConnection As Long
    Dim strBuffer As String * BUFFER_LENGTH
    Dim intBufferLength As Long
    Dim intResult As Integer
    Dim lngIndex As Long
    Dim strStatusCode As String
    Dim intStatusCode As Integer
    ' Open Session
    hSession = OpenSession
    ' Open the file
    hConnection = OpenUrl(hSession, strUrl, False)
    ' Set the default bufferlength
    intBufferLength = BUFFER_LENGTH
    ' Get the status code
    intResult = HttpQueryInfo(hConnection, http_QUERY_STATUS_CODE, ByVal strBuffer, intBufferLength, lngIndex)
    ' Valid value?
    If (intResult <> 0) Then
        ' Get the status code string
        strStatusCode = Left(strBuffer, intBufferLength)
        ' Get the integer status code
        intStatusCode = CInt(strStatusCode)
        ' Check the status code
        UrlExists = (intStatusCode = 200)
    End If
    ' Close the connection
    Call CloseHandle(hConnection)
    Call CloseHandle(hSession)
    Exit Function
    Call CloseHandle(hConnection)
    Call CloseHandle(hSession)
    ' Re-throw
    Call ReThrowError(Err)
End Function

' Download File
Public Sub DownloadFile(ByVal strUrl As String, ByVal strFilename As String)
    On Error GoTo ErrorHandling
    ' Buffer size
    Const BUFFER_SIZE As Integer = 4096
    Dim hSession As Long
    Dim hConnection As Long
    Dim strBuffer As String * BUFFER_SIZE
    Dim intFile As Integer
    Dim lngRead As Long
    Dim intResult As Integer

    ' Open session
    hSession = OpenSession()

    ' Open the file
    hConnection = OpenUrl(hSession, strUrl)
    ' Find free file
    intFile = FreeFile
    ' Create file
    Open strFilename For Binary As #intFile
            ' Read the data
            intResult = InternetReadFile(hConnection, strBuffer, BUFFER_SIZE, lngRead)
            ' Valid function?
            If (intResult <> 0) Then
                ' Valid number of bytes read?
                If (lngRead > 0) Then
                    ' Is less than buffer size?
                    If (lngRead < BUFFER_SIZE) Then
                        ' Get only the relevant data
                        strBuffer = Left(strBuffer, lngRead)
                    End If
                    ' Write the data
                    Put #intFile, , strBuffer
                End If
            End If
        Loop While (lngRead > 0)
    ' Close the file
    Close #intFile
    ' Close connection
    Call CloseHandle(hConnection)
    Call CloseHandle(hSession)
    Exit Sub
    ' Close connection
    Call CloseHandle(hConnection)
    Call CloseHandle(hSession)
    ' Rethrow
    Call ReThrowError(Err)

End Sub

Excel VBA: Re-throw error/exception

The current version of Microsoft Excel does not support the new Try/Catch blocks found in or any other modern programming languages. To handle errors within a procedure the "On Error Goto" statement must be used. When used, VBA expects the procedure to handle the error by either displaying a message visible to the user or just ignore it and continue as before.

However, if you are a Java or .NET developer you might want to restore the state of the application when a error is caught and then re-throw the error to let another part of the application handle it, just like when the "using" statement or throw/catch blocks are applied in C# code. There is no direct way of re-throwing a error in VBA, like throw or throw [Exception] in Java and .NET. The correct way of solving this challenge is to use the Err.Raise method, and specify all the existing error parameters in the following way:

Sub Test()
    On Error GoTo ErrorHandler
    ' Do Something   
    Exit Sub
    ' Restore state

    ' Re-throw/Raise existing error   
    Err.Raise Err.Number, Err.Source, Err.Description, Err.HelpFile,    Err.HelpContext
End Sub

A custom method can be made to simplifying the re-throwing of errors:

' Rethrow
Public Sub ReThrowError(ByVal objError As ErrObject)
    ' Raise   
    Err.Raise objError.Number, objError.Source, objError.Description, objError.HelpFile, objError.HelpContext
End Sub
Sunday, October 23, 2011

.NET: Alternatives to .NET Reflector

.NET Reflector is dead, long live ILSpy. Well, .NET Reflector is not really dead, as you might already know, but you now have to pay if you want to use the disassembler. Below we have listed the most common alternatives to the previously popular tool:
  1. ILSpy
  2. dotPeek
  3. JustDecompile
  4. Common Compiler Infrastructure
  5. Mono Cecil
  6. Kaliro
  7. Dotnet IL Editor (DILE)
Personally I prefer to use the ILSpy because it is open-source, and works in the similar way as the old .NET reflector.

JavaScript: Get URL Query Argument

Often I have the need to parse the URL query to retrieve the value of a specific named argument. There is no direct way of doing this is JavaScript, so this requires a custom function.

function findQueryArgument (strQuery, strArgumentName) {

    // Valid query?
    if (strQuery) {

        // Split the parameteres
        var arrParameters = strQuery.split("&");

        // Walk through the parameters
        for (var i = 0; i < arrParameters.length; i++) {
            // Get the key/value pair
            var arrPair = arrParameters[i].split("=");

            // Is this the event argument?
            if (arrPair[0] == strArgumentName) {
                // Has value?
                if (arrPair.length > 1) {
                    // Get the value
                    return arrPair[1];

                // Not set

    // Not found
    return '';

The strQuery argument is the full URL query i.e. a=val1&b=val2 etc. The full URL should not be included.
strArgumentName is the name of the argument you're looking for, i.e. a or b.

Please notice that the returned value is URL encoded. To extract the actual string value you need to use the built in JavaScript method unescape.

Here is an example:

// Query string
var strQuery = 'a=val1&b=val2';

// Get the value of the 'b' argument
var strEscaped = findQueryArgument(findQueryArgument, strQuery, 'b');

// Get the unescaped value
var strUnescaped = unescape(strEscaped);

// Display the value
Monday, October 10, 2011

Excel VBA: Sum Values IF

Below we have made a function which sums the values of one column/range given that the value of another column is equal to a fixed value. This function ignores the N/A values, which is not the case for the built-in function called SumIf in Excel. If the built-in function finds a N/A value the function returns N/A.

' SUM Value If
' Ignores N/A
Public Function SumValueIf(ByVal objRange As Range, ByVal objCriteria As Range, ByVal objSumRange As Range) As Currency
    Dim intRow As Integer
    Dim objRangeValue As Object
    Dim objCriteriaValue As Object
    Dim objValue As Object
    Dim dblValue As Currency
    Dim dblSum As Currency

    ' Get the criteria value
    objCriteriaValue = objCriteria(1, 1)

    ' Walk through the rows
    For intRow = 1 To objRange.Rows.Count Step 1
        ' Get the current value
        objRangeValue = objRange(intRow, 1)

        ' Compare values
        If (objRangeValue = objCriteriaValue) Then
            ' Get the value
            objValue = objSumRange(intRow, 1)

            ' Is valid number?
            ' Ignore any strings, #N/A, #Error, etc.
            If (IsNumeric(objValue)) Then
                ' Get the value
                dblValue = CCur(objValue)

                ' Sum
                dblSum = dblSum + dblValue
            End If
        End If

    ' Get the value
    SumValueIf = dblSum
End Function

The function can be used in the following way:

= SumValueIf(A1:A10; "abc"; B1:B10)

where A1:A10 is the compare column. Only sum the value in column B1:B10 if the value in column A is equal to "abc".

Excel VBA: Convert Variant Array to String Array

To simplify the conversion of a variant array to a string array we have made a set of utility functions.

' Array Variant to String
Public Function VariantArrayToStringArray(ByVal arrVariants As Variant) As String()
    Dim arrStrings() As String
    ' Get the string array
    Call ParamArrayToStringArray(arrVariants, arrStrings)
    ' Get the string array
    VariantArrayToStringArray = arrStrings
End Function

' Array Variant to String
Public Sub ParamArrayToStringArray(ByVal arrVariants As Variant, ByRef arrStrings() As String)
    Dim intLength As Integer
    ' Handle the array
    Call ParamArrayToStringArrayInternal(arrVariants, arrStrings, intLength)
End Sub

' Array Variant to String
Private Sub ParamArrayToStringArrayInternal(ByVal arrVariants As Variant, ByRef arrStrings() As String, ByRef intLength As Integer)
    ' Parameter is array?
    If (IsArray(arrVariants)) Then
        Dim i As Integer
        Dim objValue As Variant
        ' Walk through the specified partner objects
        For i = LBound(arrVariants) To UBound(arrVariants) Step 1
            ' Get the value
            objValue = arrVariants(i)
            ' Array to string
            Call ParamArrayToStringArrayInternal(objValue, arrStrings, intLength)
        ' Next item
        intLength = intLength + 1
        ' Expand array
        ReDim Preserve arrStrings(1 To intLength)

        ' Set the value
        arrStrings(intLength) = CStr(arrVariants)
    End If
End Sub

Excel VBA: Expand and Collapse Group

In Excel it is possible to group rows and columns. To expand or collapse a a group you can use the VBA method called ShowLevels. In the example below we are expanding a row group. We have also disabled automatic recalculation to improve the performance.

' Show Level
Public Sub ShowGroupLevel(ByVal intRowLevel As Integer)
    Dim intCalculation As XlCalculation

    ' Get the old calculation state
    intCalculation = Application.Calculation

    ' Disable re-calculation
    Application.Calculation = xlCalculationManual

    ' Show the specified level (expanded)
    On Error Resume Next
    Call ActiveWorkSheet.Outline.ShowLevels(RowLevels:=intRowLevel)
    On Error GoTo 0

    ' Restore automatic calculation
    Application.Calculation = intCalculation
End Sub

Please notice that expand and collapse work in the same way. Collapsing a group is the same as only displaying the first level. Expand is the same as showing level 2 or more.