vbapowerpointribbonx

PowerPoint Add-In Loss of RibbonUI


I have been struggling to identify the cause of an error in a PPT Add-in that is distributed across about 40 end users.

Problem: loss of the ribbon state/loss of the ribbonUI object.

For some users, eventually the Rib object becomes Nothing.

Users assure me they are not getting any run-time errors nor script errors (from COM object that we also invoke through this add-in). An unhandled error, if user hits End would expectedly cause the state loss.

None of the users have been able to reliably reproduce the scenario which causes the observed failure. This is what makes it very difficult to troubleshoot. I am hoping against hope that there is something obvious that I'm missing, or that I didn't anticipate.

How I currently handle loss or RibbonUI

In attempt to combat this, I store the object pointer to the ribbon in THREE places, this seems like overkill to me but it is still apparently not sufficient:

So you can see I've given some thought to this in attempt to replicate the way of storing this pointer the way one might store it in a hidden worksheet/range in Excel.

Additional information

I can see from robust client-side logging that this the error appears to happen usually but not always during the procedure below, which is used to refresh/invalidate the ribbon and its controls.

This procedure is called any time I need to dynamically refresh the ribbon or part of its controls:

Call RefreshRibbon(id)

The error appears to (sometimes, I can't stress this enough: the error cannot be replicated on-demand) happen during a full refresh, which is called like:

Call RefreshRibbon("")

This is the procedure that does the invalidation:

Sub RefreshRibbon(id As String)

    If Rib Is Nothing Then
        If RibbonError(id) Then GoTo ErrorExit
    End If

    Select Case id
        Case vbNullString, "", "RibbonUI"
            Call Logger.LogEvent("RefreshRibbon: Rib.Invalidate", Array("RibbonUI", _
                                            "Ribbon:" & CStr(Not Rib Is Nothing), _
                                            "Pointer:" & ObjPtr(Rib)))
            Rib.Invalidate

        Case Else
            Call Logger.LogEvent("RefreshRibbon: Rib.InvalidateControl", Array(id, _
                                            "Ribbon:" & CStr(Not Rib Is Nothing), _
                                            "Pointer:" & ObjPtr(Rib)))
            Rib.InvalidateControl id
    End Select

    Exit Sub

ErrorExit:

End Sub

As you can see, the very first thing I do in this procedure is test the Rib object for Nothing-ness. If this evaluates to True, then the RibbonUI object has somehow been lost.

The error function then attempts to re-instantiate the ribbon: first from cbRibbon.RibbonUI, then from the cbRibbon.Pointer and if both of those fails, then from the CustomDocumentProperties("RibbonPointer") value. If neither of these succeeds, then we display a fatal error and the user is prompted to close the PowerPoint application. If any one of these succeeds, then the ribbon is reloaded programmatically and everything continues to work.

Here is the code for that procedure. Note that it calls several other procedures which I have not included code for. These are helper functions or logger functions. The .GetPointer method actually invokes the WinAPI CopyMemory function to reload the object from its pointer value.

Function RibbonError(id As String) As Boolean
'Checks for state loss of the ribbon
Dim ret As Boolean

If id = vbNullString Then id = "RibbonUI"

Call Logger.LogEvent("RibbonError", Array("Checking for Error with Ribbon" & vbCrLf & _
                                            "id: " & id, _
                                            "Pointer: " & ObjPtr(Rib), _
                                            "cbPointer: " & cbRibbon.Pointer))

If Not Rib Is Nothing Then
    GoTo EarlyExit
End If

On Error Resume Next

    'Attempt to restore from class object:
    Set Rib = cbRibbon.ribbonUI

    'Attempt to restore from Pointer reference if that fails:
    If Rib Is Nothing Then
        'Call Logger.LogEvent("Attempt to Restore from cbRibbon", Array(cbRibbon.Pointer))
        If Not CLng(cbRibbon.Pointer) = 0 Then
            Set Rib = cbRibbon.GetRibbon(cbRibbon.Pointer)
        End If
    End If

    'Attempt to restore from CDP

    If Rib Is Nothing Then
        'Call Logger.LogEvent("Attempt to Restore from CDP", Array(MyDoc.CustomDocumentProperties("RibbonPointer")))
        If HasCustomProperty("RibbonPointer") Then
            cbRibbon.Pointer = CLng(MyDoc.CustomDocumentProperties("RibbonPointer"))
            Set Rib = cbRibbon.GetRibbon(cbRibbon.Pointer)

        End If
    End If

On Error GoTo 0

If Rib Is Nothing Then
    Debug.Print "Pointer value was: " & cbRibbon.Pointer
    'Since we can't restore from an invalid pointer, erase this in the CDP
    ' a value of "0" will set Rib = Nothing, anything else will crash the appliation
    Call SetCustomProperty("RibbonPointer", "0")
Else
    'Reload the restored ribbon:
    Call RibbonOnLoad(Rib)

    Call SetCustomProperty("RibbonPointer", ObjPtr(Rib))

    cbRibbon.Pointer = ObjPtr(Rib)
End If

'Make sure the ribbon exists or was able to be restored
ret = (Rib Is Nothing)

If ret Then
    'Inform the user
    MsgBox "A fatal error has been encountered. Please save & restart the presentation", vbCritical, Application.Name
    'Log the event to file
    Call Logger.LogEvent("RibbonError", Array("FATAL ERROR"))

    Call ReleaseTrap

End If

EarlyExit:

    RibbonError = ret

End Function

All of this works perfectly well in theory and in fact I can straight-up kill run-time (by invoking the End statement or otherwise) and these procedures reset the ribbon as expected.

enter image description here

So, what am I missing?


Solution

  • OK I forgot about this... while I still have not pinpointed the error I have some ideas that users are simply not reporting unhandled runtime errors and instead they're hitting "End" when prompted by PowerPoint.

    I'm reasonably certain that is the cause and I have confirmation that in many cases, that sort of error precedes the "crash", so I'm updating to resolve that soon.

    Otherwise, here is the method I ultimately have been using for several months, with success.

    Create a procedure that writes the Pointer value of the ribbon on the user's machine. I didn't want to do this, but ultimately had to:

    Sub LogRibbon(pointer As Long)
        'Writes the ribbon pointer to a text file
        Dim filename As String
        Dim FF As Integer
    
        filename = "C:\users\" & Environ("username") & "\AppData\Roaming\Microsoft\AddIns\pointer.txt"
    
        FF = FreeFile
        Open filename For Output As FF
        Print #FF, pointer
        Close FF
    
    End Sub
    

    In the ribbon's _OnLoad event handler, I call the LogRibbon procedure:

    Public Rib As IRibbonUI
    Public cbRibbon As New cRibbonProperties
    Sub RibbonOnLoad(ribbon As IRibbonUI)
    'Callback for customUI.onLoad
    
    
        Set Rib = ribbon
    
        Call LogRibbon(ObjPtr(Rib))
    
        'Store the properties so we can easily access them later
        cbRibbon.ribbonUI = Rib
    
    
    End Sub
    

    I created a class object to store some information about the ribbon to avoid repeated and slow calls to an external API, but for this purpose you can create a class that stores just the pointer value. That is referenced above in the cbRibbon.ribbonUI = Rib. This GetRibbon method of this class uses the CopyMemory function from WinAPI to restore the object from it's pointer.

    Option Explicit
    
    Private Declare Sub CopyMemory Lib "kernel32" Alias _
        "RtlMoveMemory" (destination As Any, source As Any, _
        ByVal length As Long)
    
    
    'example ported from Excel:
    'http://www.excelguru.ca/blog/2006/11/29/modifying-the-ribbon-part-6/
    Private pControls As Object
    Private pRibbonUI As IRibbonUI
    Private pPointer As Long
    
    Sub Class_Initialize()
        'Elsewhere I add some controls to this dictionary so taht I can invoke their event procedures programmatically:
        Set pControls = CreateObject("Scripting.Dictionary")
    
        Set pRibbonUI = Rib
    
        Call SaveRibbonPointer(Rib)
    
        pConnected = False
    End Sub
    
    
    '#############################################################
    'hold a reference to the ribbon itself
        Public Property Let ribbonUI(iRib As IRibbonUI)
            'Set RibbonUI to property for later use
            Set pRibbonUI = iRib
    
        End Property
    
        Public Property Get ribbonUI() As IRibbonUI
            'Retrieve RibbonUI from property for use
            Set ribbonUI = pRibbonUI
        End Property
    
    'http://www.mrexcel.com/forum/excel-questions/518629-how-preserve-regain-id-my-custom-ribbon-ui.html
    Public Sub SaveRibbonPointer(ribbon As IRibbonUI)
        Dim lngRibPtr As Long
        ' Store the custom ribbon UI Id in a static variable.
        ' This is done once during load of UI.
    
        lngRibPtr = ObjPtr(ribbon)
    
        cbRibbon.pointer = lngRibPtr
    
    End Sub
    Function GetRibbon(lngRibPtr As Long) As Object
        'Uses CopyMemory function to re-load a ribbon that
        ' has been inadvertently lost due to run-time error/etc.
        Dim filename As String
        Dim ret As Long
        Dim objRibbon As Object
    
        filename = "C:\users\" & Environ("username") & "\AppData\Roaming\Microsoft\AddIns\pointer.txt"
    
        On Error Resume Next
        With CreateObject("Scripting.FileSystemObject").GetFile(filename)
            ret = .OpenAsTextStream.ReadLine
        End With
        On Error GoTo 0
    
        If lngRibPtr = 0 Then
            lngRibPtr = ret
        End If
    
        CopyMemory objRibbon, lngRibPtr, 4
        Set GetRibbon = objRibbon
        ' clean up invalid object
        CopyMemory objRibbon, 0&, 4
        Set objRibbon = Nothing
    
    End Function
    
    
    '##############################################################
    ' Store the pointer reference to the RibbonUI
        Public Property Let pointer(p As Long)
            pPointer = p
        End Property
        Public Property Get pointer() As Long
            pointer = pPointer
        End Property
    
    '#############################################################
    'Dictionary of control properties for Dropdowns/ComboBox
        Public Property Let properties(p As Object)
            Set pProperties = p
        End Property
        Public Property Get properties() As Object
            Set properties = pProperties
        End Property
    

    Then, I have a function which checks for loss of ribbon, and restores from the pointer value. This one actually invokes the OnLoad procedure, which we can do since we have an object variable (or class object property) representing the Ribbon object).

    Function RibbonError(id As String) As Boolean
    'Checks for state loss of the ribbon
    Dim ret As Boolean
    Dim ptr As Long
    Dim src As String
    
    On Error Resume Next
    
    If Not Rib Is Nothing Then
        GoTo EarlyExit
    End If
    
    If Rib is Nothing then
        ptr = GetPointerFile
        cbRibbon.pointer = ptr
        Set Rib = cbRibbon.GetRibbon(ptr)
    End If
    On Error GoTo 0
    
    'make sure the ribbon has been restored or exists:
    ret = (Rib is Nothing)
    
    If Not ret then
        'Reload the restored ribbon by invoking the OnLoad procedure
        ' we can only do this because we have a handle on the Ribbon object now
        Call RibbonOnLoad(Rib)
        cbRibbon.pointer = ObjPtr(Rib) 'store the new pointer
    Else
        MsgBox "A fatal error has been encountered.", vbCritical
    End If
    
    EarlyExit:
    RibbonError = ret
    End Function
    

    Call on the RibbonError function any time you are going to refresh the ribbon through either Invalidate or InvalidateControl methods.

    The code above may not 100% compile -- I had to modify it and trim some stuff out, so let me know if you have any problems trying to implement it!