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:
- A class object called
cbRibbon
has a property .RibbonUI
which is assigned; Set cbRibbon.RibbonUI = Rib
during the ribbon's onLoad
callback procedure. So we have a byRef
copy of the object itself. If the ribbon is nothing, theoretically I can Set rib = cbRibbon.RibbonUI
and this works unless cbRibbon
object is also out of scope.
- The
cbRibbon
object has property .Pointer
which is assigned: cbRibbon.Pointer = ObjPtr(Rib)
.
- A
CustomDocumentProperty
called "RibbonPointer" is also used to store a reference to the object pointer. (Note: This persists even beyond state loss)
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.
So, what am I missing?
See Question&Answers more detail:
os