1

The PHB wants me to create a ribbon toggle button for Microsoft Word that:

  • when pressed, restricts editing to filling in forms and protects the document without a password.
  • when unpressed, unprotects the document (without a password).

I have the following customUI.xml:

<customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui" onLoad="RibbonOnLoad">
    <commands>
        <command idMso="ProtectOrUnprotectDocument" onAction="ProtectOrUnprotectDocumentOnAction"/>
    </commands>
    <ribbon>
        <tabs>
            <tab id="LawTab" label="Law">
                <group id="ProtectGroup" label="Protect">
                    <toggleButton id="ToggleProtectionButton" imageMso="GreenBall" label="Protection" getPressed="ToggleProtectionButtonGetPressed" onAction="ToggleProtectionButtonOnAction"/>
                    <button id="InvalidateRibbonButton" imageMso="Refresh" label="Invalidate Ribbon" onAction="InvalidateRibbonButtonOnAction"/>
                </group>
            </tab>
        </tabs>
    </ribbon>
</customUI>

and the following VBA code:

Private ribbon As IRibbonUI

Sub InvalidateRibbonButtonOnAction(control As IRibbonControl)
    ribbon.Invalidate
End Sub

Sub ProtectOrUnprotectDocumentOnAction(control As IRibbonControl, ByRef cancelDefault)
    ribbon.Invalidate
    cancelDefault = False
End Sub

Sub RibbonOnLoad(ActiveRibbon As IRibbonUI)
    Set ribbon = ActiveRibbon
End Sub

Sub ToggleProtectionButtonGetPressed(control As IRibbonControl, ByRef returnValue)
    returnValue = ActiveDocument.ProtectionType <> wdNoProtection
End Sub

Sub ToggleProtectionButtonOnAction(control As IRibbonControl, ByVal pressed As Boolean)
    If pressed Then
        ActiveDocument.Protect wdAllowOnlyFormFields
    Else
        ActiveDocument.Unprotect
    End If
End Sub

I can repurpose the built-in ProtectOrUnprotect command so the corresponding built-in button invalidates the ribbon and consequently updates my custom button, but if I protect/unprotect with the built-in task pane (Review > Restrict Editing) or programmatically (ActiveDocument.Protect/Unprotect), my custom button stays ignorant of the change. How can I listen to the document-level event to protect/unprotect so I can update the state of my toggle button?

Homer
  • 188
  • 1
  • 8
  • +1 for a good question. There are not many resources for ribbon manipulation. I will see if I can figure it out if I get a few free minutes... – David Zemens Apr 14 '14 at 18:19
  • Please insert a message box in the `ToggleProtectionButtonGetPressed` function. Are you able to invoke this function through user action in Word? I am not able to do so. If you are, please let me know how you do it and I will keep trying. Otherwise, your solution may require an Add-In similar to what is needed for PowerPoint ([here](http://www.pptfaq.com/FAQ00004_Make_your_VBA_code_in_PowerPoint_respond_to_events.htm)), **IF** it is even possible to respond to this particular event (it may not be)... – David Zemens Apr 14 '14 at 21:32
  • I created an Invalidate Ribbon button. To test it, use the built-in task pane (Review > Restrict Editing) to start/stop protection. My custom button will ignore the actual protection state until you press the Invalidate Ribbon button. – Homer Apr 15 '14 at 17:15

2 Answers2

2

How about forcing the built-in button to call your code?

To summarize, you can override the built-in Sheet Protect button to use your code (which hopefully causes your button to toggle) by adding the following to your ribbon XML (see the link for details):

<commands>
    <command idMso="ProtectOrUnprotectDocument" onAction="ToggleProtectionButtonOnAction"/>
</commands>
Community
  • 1
  • 1
Blackhawk
  • 5,984
  • 4
  • 27
  • 56
  • Repurposing the ProtectOrUnprotectDocument command works . . . but only for that command. If I protect/unprotect with the built-in task pane (Review > Restrict Editing) or programmatically (ActiveDocument.Protect/Unprotect), my custom button stays ignorant of the change. – Homer Apr 15 '14 at 17:23
  • @Homer and David, thanks for the headsup, I think I confused myself by looking at the Excel example I linked. I'm updating the answer to use the "ProtectOrUnprotectDocument" id for posterity's sake. – Blackhawk Apr 15 '14 at 19:01
  • @Homer How about adding an additional command line for "ReviewRestrictFormatting"? [This page](http://msdn.microsoft.com/en-us/library/dd950659(v=office.12).aspx) seems to have a pretty complete list of the button ids. I don't see any events that would be directly available in VBA :'( – Blackhawk Apr 15 '14 at 19:07
  • That still misses protecting/unprotecting programmatically (ActiveDocument.Protect/Unprotect). I could hack together the observer pattern and tell everyone to always use a layer that calls the built-in Protect/Unprotect behind the scenes and never use the built-in Protect/Unprotect directly . . . but no. – Homer Apr 15 '14 at 20:19
  • @Homer "...but no" - Agreed :P – Blackhawk Apr 15 '14 at 20:23
1

I think Blackhawk's answer is on the right track: override the built-in command's onAction procedure. However, his XML example is for MS Excel and will not work in MS Word. This can be tweaked pretty easily, but unfortunately, I can't figure out how to solve this particular problem:

but if I use the built-in ProtectOrUnprotectDocument button to protect/unprotect, my custom button stays ignorant of the change. How can I listen to the document-level event to protect/unprotect so I can update the state of my toggle button?

There is no document-level event, even using a WithEvents application class, which responds to a change in the document's ProtectionType (in theory you should probably be able to use the ribbon .InvalidateControl method).

So the question (and possible resolution) is why you need a toggle button, when you can simply use the built-in button and hijack it's functionality with your own procedure to protect/unprotect as needed. You can even place the built-in button within your custom menu.

However, this looks promising:

http://sourcedaddy.com/ms-excel/getting-and-changing-control-values.html

REVISED

After some discussion and trial and error (you did a LOT of this on your own, and figured out what I could not), let's try this which is working for me.

Here is XML (you may need to modify the schemas if you're using older version).

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
   <customUI onLoad="RibbonOnLoad" xmlns="http://schemas.microsoft.com/office/2009/07/customui">
    <commands>
        <command idMso="ProtectOrUnprotectDocument" onAction="ToggleProtectionButtonOnAction" />
    </commands>
    <ribbon>
        <tabs>
            <tab id="LawTab" label="Law">
                <group id="ProtectGroup" label="Protect">
                    <toggleButton id="ToggleProtectionButton" imageMso="GreenBall" label="Protection" getPressed="ToggleProtectionButtonGetPressed" onAction="ToggleProtectionButtonOnAction"/>
                    <button id="InvalidateButton" imageMso="Refresh" label="Invalidate" onAction="InvalidateButtonOnAction"/>
                </group>
            </tab>
        </tabs>
    </ribbon>
</customUI>

What I did is to send both the ProtectOrUnprotectDocument command and your custom toggle button, ToggleProtectionButton, to the same onAction handler. Then I use some logic (which seems to be working) to selectively invalidate the ribbon, while a boolean variable captures the document's protection state, throughout the procedures and callbacks.

Here is VBA:

Option Explicit
Dim ribbon As IRibbonUI
Dim protectButton As IRibbonControl
Dim IsProtected As Boolean

Sub InvalidateRibbonButtonOnAction(control As IRibbonControl)
'MsgBox "Invalidate"
    ribbon.Invalidate
End Sub

Sub RibbonOnLoad(ActiveRibbon As IRibbonUI)
'MsgBox "onLoad"
    Set ribbon = ActiveRibbon
    IsThisDocumentProtected
End Sub

Sub ToggleProtectionButtonGetPressed(control As IRibbonControl, ByRef returnValue)
'MsgBox "GetPressed"
    IsThisDocumentProtected
    returnValue = IsProtected
End Sub

Sub ToggleProtectionButtonOnAction(control As IRibbonControl, ByVal pressed As Boolean)
    IsThisDocumentProtected
    If pressed Then
        If Not IsProtected Then
            ActiveDocument.Protect wdAllowOnlyFormFields
        Else
            ActiveDocument.Unprotect
        End If
    Else
        If IsProtected Then
            ActiveDocument.Unprotect
        End If

    End If
    If control.Id = "ProtectOrUnprotectDocument" Then
    '    MsgBox "Got here!"
    '   This will force ribbon invalidate only for the native command
        ProtectOrUnprotectDocumentOnAction control, False
    End If
End Sub


''''' This procedure is NOT a callback, but is called from the callbacks:

Private Sub IsThisDocumentProtected()
'''' Assigns value to module-level boolean variable for use throughout the module
    IsProtected = ActiveDocument.ProtectionType <> wdNoProtection
End Sub

''''' This is NOT a callback, either, but is invoked only for particular control press:
Private Sub ProtectOrUnprotectDocumentOnAction(control As IRibbonControl, ByRef cancelDefault)
    ribbon.Invalidate
    cancelDefault = False
End Sub

Limitations

if I protect/unprotect with the built-in task pane (Review > Restrict Editing) or programmatically (ActiveDocument.Protect/Unprotect), my custom button stays ignorant of the change.

This will not work for protection that is applied programmatically. As for the Review > Restrict Editing, I think you simply need to hijack that command's onAction procedure in the same way we have done, above, by adding another command to the XML and referring it to the same onAction procedure.

David Zemens
  • 53,033
  • 11
  • 81
  • 130
  • I updated my callbacks after referencing [Customizing the 2007 Office Fluent Ribbon for Developers (Part 3 of 3)](http://msdn.microsoft.com/en-us/library/aa722523(v=office.12).aspx). – Homer Apr 15 '14 at 18:30
  • For this particular case (ProtectOrUnprotectDocument), repurposing the built-in button might work. However, I also have other custom controls that prompt the author for various data then insert boilerplate; they should listen to the protection state and enable/disable accordingly. (I wish I could just use the built-in AutoText gallery and content controls, but the PHB wants the team to "upgrade" a legacy CommandBars-based interface to a Ribbon-based one . . . "without changing anything.") – Homer Apr 15 '14 at 18:32
  • Are you any closer to the solution now? I see you've updated your code again in the original question. Try as I might, I was not able to *assign* a state to the toggle button... I have been pretty busy this afternoon, may have a chance to look at it again tonight. – David Zemens Apr 15 '14 at 20:52
  • 1
    I was able to update the state of my custom controls. (See the most recent code in the original post.) Invalidating the ribbon calls all get* callbacks; in a getPressed callback, assign whether a control should be pressed to the returnValue parameter. After removing some cruft and refactoring, I think I can keep the code smelliness of mixing functional code and UI code at a tolerable level. – Homer Apr 15 '14 at 21:57
  • OK cool, so that is a start. You're able to update it but still not able to "listen" to the document's protected state? – David Zemens Apr 15 '14 at 22:09
  • Correct. I'd like to listen to protect/unprotect (and other document-level events beyond Close, New, and Open). – Homer Apr 16 '14 at 00:10
  • 1
    well the issue is whether there is an event that you can actively listen for. There does not appear to be such an event. So the next-best solution, if possible, is to hijack the `ProtectOrUnprotectDocument` command's `onAction`. I think I have something that is working for me. See revised answer. – David Zemens Apr 16 '14 at 02:04
  • 1
    I've looked for a long time and figured as much. Hacking together an observer/pub-sub module would solve this nicely but lengthily, so I'll just stick with the code in the original post. Over time, I think the best way to proceed is helping the PHB and the rest of the office learn more about built-in UI. I try :) – Homer Apr 16 '14 at 16:55
  • *I try :)* -- exactly!! :) Well good, it sounds like you have something workable. I did go one better in the latest revision by showing how to redirect the `onAction` of existing commands so that your buttons/etc. will update accordingly. In any case, if this has helped you solve the problem do consider up-voting or accepting the answer. Cheers! – David Zemens Apr 16 '14 at 22:15