Custom Ribbon Help for Office 2010 VSTO Add-ins






4.59/5 (6 votes)
Providing custom context sensitive help for VSTO add-in Ribbon controls.
Introduction
If you have ever written an Office add-in that includes a Ribbon, you may have noticed the "Press F1 for help" ScreenTip that pops up when you hover over it. If you're like me, you'll have wondered how to get this to display some custom help and be slightly dissappointed to find that you can't.
This articles describes the settings that affect ScreenTips and a possible method that can be used to display custom help for Ribbon controls in custom VSTO application level add-ins.
Background
VSTO application add-ins allow developers to extend Office applications. Whilst the models for each Office application vary, the core implementations of controls such as the Ribbon are the same.
One key feature of the Ribbon control is the ScreenTip that allows you to enter a description of each controls functionality. For a custom add-in, the bottom section of a ScreenTip shows a cog icon, your friendly add-in name and a "Press F1 for add-in help" message:

You can control whether or not ScreenTips and their SuperTips are displayed via the "File...Options...General" settings.
For any custom add-in, pressing F1 opens up the generic "View, manage, and install add-ins in Office programs" help topic. The aim of this article to show a way in which this behaviour can be altered to show custom help.
Office applications use Microsoft Active Accessibility (MSAA) to expose UI information to the outside world. The use of MSAA is central to the implementation of the code in this article. See the code project article UI Automation Using Microsoft Active Accessibility (MSAA) for an introduction to this topic.
Using the code
The RibbonHelpContext
raises the HelpActivated
event whenever the user presses the F1 key and the Ribbon is in the context of
custom help. This event defines what's in context using the HelpActivatedEventArgs
class.
You'll need to define what's providing custom help using an instance of IHelpCollection,
which
exposes a dictionary of
RibbonHelpItem
items. This lets the context know which controls it's providing custom help for (an
undesired side affect of the code is that you can provide help for any part of the Ribbon).
A
RibbonHelpItem
describes two properties: a Key
and a HelpId
. They Key
describes the controls path, a
colon seperated list of the id's in the hierarchy. The HelpId
is a just that, an identifer that you can use to
activate the required help behaviour.
In my samples I've implemented
IHelpCollection
by serializing an Xml file:
<?xml version="1.0" encoding="utf-8"?>
<Help xmlns="http://addin-help">
<!-- Repeat this for each ribbon in your add-in -->
<AddInHelp key="OfficeHelpSampleRibbon">
<!-- An entry for each control you want to provide custon help for -->
<HelpItem key="OfficeHelpSampleRibbon:group1:button1" helpId="1001" />
</AddInHelp>
</Help>
To use the RibbonHelpContext
hook into the add-ins Startup and Shutdown events to
initialize and dispose an instance of RibbonHelpContext
. It's especially important to call Dispose()
in the
Shutdown event to ensure that the windows api hooks are correctly removed:
//...
using RibbonHelp.Core;
//...
namespace AddIns.Namespace
{
public partial class ThisAddIn
{
private RibbonHelpContext ribbonHelp;
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
// get an instance of IHelpCollection from somewhere...
var hc = HelpCollection.FromXmlString(Resources.Help);
// initialize the local instance
ribbonHelp = new RibbonHelpContext(hc);
// decide what you want to do when the help is activated...
ribbonHelp.HelpActivated += (o, args) => Debug.WriteLine(args.HelpItem.HelpId);
}
private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
{
// this bit is important to ensure the api hooks are removed correctly
if (ribbonHelp != null) ribbonHelp.Dispose();
}
//...
}
}
Running the samples
The sample solution contains an application level add-in for Excel, Word, Powerpoint and Outlook 2010. Each sample has
a Ribbon, and uses an instance of RibbonHelpContext
as described above.
When running any of the samples in debug mode you can use the trace pane to get an overview of the current context. You can show/hide the trace pane by clicking the "Toggle Trace Pane" button in the top left of each Ribbon sample:

Whilst running any of the samples in debug, if the control context label is green then pressing F1 will result in a custom help topic being displayed.
Implementation
The bulk of the work is done by two classes, RibbonShowHideContext
and RibbonFocusContext
.
Each of these classes hook into MSSA events using the SetWinEventHook
and
use the
AccessibleObjectFrom
* P/Invoke api calls. Both classes subclass RibbonContextBase
, which does the work
of hooking up the events.
//..
public abstract class RibbonContextBase : IRibbonContext
{
protected IntPtr Hook { get; private set; }
protected IHelpItems HelpCollection { get; private set; }
private GCHandle hookDelegateHandle;
private readonly WinEventProc hookDelegate;
internal IWinApi Win32;
internal RibbonContextBase(IHelpItems helpCollection, IWinApi win32,
WinEvent eventMin, WinEvent eventMax)
{
if (helpCollection == null)
throw new ArgumentNullException("helpCollection");
if (win32 == null)
throw new ArgumentNullException("win32");
this.HelpCollection = helpCollection;
this.Win32 = win32;
this.hookDelegate = Callback;
this.hookDelegateHandle = GCHandle.Alloc(this.hookDelegate);
this.Hook = this.Win32.SetWinEventHook(
(uint)eventMin,
(uint)eventMax,
IntPtr.Zero, hookDelegate, 0, (uint)AppDomain.GetCurrentThreadId(), 0);
}
//..
internal abstract void Callback(IntPtr hWinEventHook, WinEvent eventType,
IntPtr hwnd, uint idObject,
uint idChild, uint dwEventThread,
uint dwmsEventTime);
//..
}
The combination of RibbonShowHideContext
and RibbonFocusContext
allow the tracking of both mouse and keyboard
interaction.
RibbonShowHideContext
RibbonShowHideContext
tracks two MSAA events, EVENT_OBJECT_SHOW
and EVENT_OBJECT_HIDE
. It doesn't track every single MSAA show and hide event, there are simply to many off them and it would be
inefficient.
To prevent this, RibbonShowHideContext
uses an instance of MouseContext
, which is a straight
forward implmentation of a windows hook, using the SetWindowsHookEx
api call that tracks the WM_MOUSEMOVE
message. Whenever the mouse is in the context of the Ribbon, ribbonshowhidecontext
will process
messages and set any required context. It uses the AccessibleObjectFromPoint
P/Invoke api to establish the
control in context.
//..
public class RibbonShowHideContext : RibbonContextBase
{
//..
internal override void Callback(IntPtr hWinEventHook, WinEvent eventType, IntPtr hwnd,
uint idObject, uint idChild, uint dwEventThread, uint dwmsEventTime)
{
//-> don't do anything if we're not in the ribbon
if (!this.mouseInContext) return;
//..
Point cursorPosition;
if (Win32.GetCursorPos(out cursorPosition) == 0) return;
IAccessible accFromPoint;
object childFromPoint;
Win32.AccessibleObjectFromPoint(cursorPosition, out accFromPoint, out childFromPoint);
//..
}
//..
}
RibbonFocusContext
RibbonFocusContext
tracks a single MSAA event, EVENT_OBJECT_FOCUS
. It processes messages and sets any required context.
Unlike the RibbonShowHideContext
, it does process every EVENT_OBJECT_FOCUS
message.
It uses the AccessibleObjectFromEvent
P/Invoke api to establish the control in context.
//..
public class RibbonFocusContext : RibbonContextBase
{
//..
internal override void Callback(IntPtr hWinEventHook, WinEvent eventType, IntPtr hwnd,
uint idObject, uint idChild, uint dwEventThread, uint dwmsEventTime)
{
//..
IAccessible accEventObject;
object child;
if(Win32.AccessibleObjectFromEvent(hwnd, idObject,
idChild, out accEventObject, out child) != 0x0)
return;
//..
}
//..
}
Both classes are effectively navigating the context controls parent hierarchy/path and then checking if that path is in
the provided IHelpCollection
.
The path is found by navigation the IAccessible
interface of the object discovered from the relevant
AccessibleObjectFrom*
P/Invoke function.
//..RibbonContextBase
private static readonly string[] RootList =
new[] { RibbonConstants.RibbonLower, RibbonConstants.Ribbon, RibbonConstants.RibbonTabs };
private List<string> GetAccessiblePathContext(IAccessible accessibleObj)
{
var contextName = accessibleObj.accName[0];
var lastName = this.ContextControl;
var newContextPathList = new List<string>();
if (!string.IsNullOrEmpty(contextName))
{
//-> Don't repeat
if (contextName == this.ContextControl) return this.contextPathList;
lastName = contextName;
newContextPathList.Add(contextName);
}
IAccessible accParent = accessibleObj.accParent;
while (accParent != null)
{
contextName = accParent.accName[0];
if (contextName.IsNullOrEmpty())
{
accParent = accParent.accParent;
continue;
}
//-> For when the Ribbon is collapsed
if (contextName == RibbonConstants.RibbonTabs && newContextPathList.Count == 1)
{
var stateUint = Convert.ToUInt32(accessibleObj.accState[0]);
var selectable = AccessibleState.HasState(stateUint,
Win.AccessibleStates.STATE_SYSTEM_SELECTABLE);
if (selectable)
{
newContextPathList.Insert(0, "*Tab*");
break;
}
}
//-> once we hit any Ribbon root container we're not interested in any more hierarchy
if (contextName.EqualsAny(StringComparison.OrdinalIgnoreCase, RootList))
break;
if (contextName != lastName)
{
newContextPathList.Add(contextName);
lastName = contextName;
}
accParent = accParent.accParent;
}
newContextPathList.Reverse();
return newContextPathList;
}
Points of Interest
ScreenTip style settings and behaviour
An interesting side effect of using the MSAA events to track context is how the behaviour almost naturally reflects the ScreenTip style options setting. There are three settings, and there is one subtle difference in how these settings affect Ribbon controls that cannot accept keyboard focus. A Ribbon control will only be ready to accept keyboard focus once something has been selected.

Apart from the "Don't show ScreenTips" setting, when you press F1 in the context of any Ribbon item you will be directed to the "View, manage, and install add-ins in Office programs" help topic.
When "Don't show ScreenTips" is on, Ribbon controls that have not recieved keyboard focus will direct you to the Office help index.
The behaviour of the custom help provider ends up working in the much the same way. You will be directed to the custom help topic when the Ribbon has recieved keyboard focus, otherwise you wil be directed to the Office help index.
One exception to this rule appears to be the RibbonComboBox
. The items in the RibbonComboBox
don't raise the
EVENT_OBJECT_FOCUS
event, so they can only be tracked by the RibbonShowHideContext
, which is dependant on the ScreenTip
behaviour.
This behaviour makes sense, given that we are using the show and hide events to track context, if there's nothing to show
there's nothing for us to track. It also shows Ribbon controls only raise EVENT_OBJECT_FOCUS
events
once they are ready to accept keyboard focus - as per it's documented behaviour.
It's a bit too brutal
It's probably obvious, but tracking the Ribbon in this way and overriding the F1 key means you can pretty much change the help behaviour of the whole ribbon. Although this may sound like a neat side affect, I actually feel a little uncomfortable about it.
When implmenting keyboard hooks, there are some rules you are recommened to follow, one of them being always pass on the
call to the next handler in the chain using CallNextHookEx
. In order to stop the default Excel help opening
in our custom help context, the call is not passed on when custom help is activated.
This may lead to other processses that rely on the F1 key to not function. In addition, we can never be sure if any other process is stopping our behaviour from working. I've only tested this code on a developer Windows 7 workstation with Office 2010. I'm sure there are some security and environmental considerations that have not been taken into account.
I wish I didn't have to think about it
Ultimately, I want to write add-ins that look and feel professional. I wish there was a better, documented, built in way of doing this. It could just be me, but I think devs want the option to provide and intergate custom help in some way and the "Press F1 for add-in help" ScreenTip is neither friendly or intuitive to both developers and end-users.
History
First version.