Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Simple, Integrated WPF Help System

0.00/5 (No votes)
1 Mar 2010 1  
Give your users quick, visual help without forcing them to leave your app.

Introduction

I was recently confronted with the common problem of creating a help system for a WPF application I created. The program was a "standard" business application (buttons, lists, etc.), so I figured there was probably some built-in support system or new standard to easily provide user help. To my surprise, there was no clear standard, so I decided to create my own.

Background

Initially, I assumed WPF would have built-in support for a help system standard such as CHM files. After some investigation, I discovered the AutomationProperties.HelpText attached property, but found that the consensus seems to be that it is an incomplete feature or was added for future use. I could not find anything in the framework that actually used the HelpText property directly and, for now, it is obviously meant to be consumed by a client application rather than drive an automated system (silly me, I took that whole "Automation" thing literally).

Potential Solutions

Before creating my solution, I thought of some of the existing ways to provide a help system to users (for business reasons, third party and/or Open Source solutions were pretty much not an option). This partial list addresses many of the issues I encountered:

  1. External documentation
    • Pros:
      • Ummm...give me a minute...
    • Cons:
      • As soon as you put something in a document, it is outdated.
      • User must find and open an external file, then switch between the app and the document.
      • I don't want to spend my day pasting screenshots into Word.
  2. Tooltips
    • Pros:
      • Familiar to users.
      • Built into WPF.
    • Cons:
      • No way to tell if a control has a tooltip or not; user must hover and wait on every control.
      • Tooltips are generally meant to contain only a few words, and I needed to potentially have two or three sentences.
      • "Always on" - once you know what a control does, you most likely will not need the tooltip again. However, if you pause over the control, it will pop up no matter what. This can be annoying if there is a lot of text in the tooltip.
  3. Sidebar help (like MS Office)
    • Pros:
      • Familiar to users.
      • Can be turned off/hidden.
      • Made to handle larger amounts of text.
    • Cons:
      • Best suited for a robust, fully indexed, and linked help system.
      • Changes existing UI layouts.

After looking at these options (and others), I decided that I liked the way tooltips worked the best. They tell the user exactly what will happen when they act on the control they are pointing at. The problem, however, is that I wanted to be able to turn them off like you can with the sidebar help.

My Solution

I decided that any sort of external solution was out of the question. WPF has many advanced features, and there is no reason I should force users to navigate away from my application or even open up a new window. I also did not want the help to ever "be in the way", so I knew it had to be able to be turned on and off easily. What I came up with was this simple, yet powerful, solution.

A sample screenshot

Hitting the F1 key puts a yellow highlight around the controls that have help available. When the mouse is placed over the control, the help text is displayed in a tooltip like fashion (there is no delay, however) using the handy Popup class. When the user is done, hitting F1 again removes the highlight and the help is no longer displayed. In the sample above, if the mouse is over button "Two", the help for the group box is displayed because "Two" has no help available. When you move the mouse to button "Three", the help for that button is displayed. Of course, when the mouse is moved over the canvas or a control with no help, the help bubble disappears.

How it Works

The code to make this work is rather simple. Let me first say, however, that this solution is not optimized, and I'm sure the WPF disciples among us will probably come up with better ways to walk the visual tree or use a visual effect that is more efficient (which BitmapEffects are not). I will gladly listen to suggestions.

Key classes/properties

  • System.Windows.Automation.AutomationProperties.HelpText
  • System.Windows.Controls.Primitives.Popup
  • System.Windows.Media.VisualTreeHelper
  • System.Windows.Media.Effects.OuterGlowBitmapEffect

Step 1 - XAML

<Window ...usual stuff... Name="winMain" KeyDown="winMain_KeyDown">
    <Canvas Name="canvMain">
        <Button Content="No Help" ... />
        <Button Content="Has Help" AutomationProperties.HelpText="I have help" ... />
    ...just the basic controls...
</Window>

Step 2 - Members / Extension Method

public static class StringUtils
{
    public static bool IsNothing(this string value)
    {
        return value == null || value.Trim().Length == 0;
    }
}

// Members in Window1
private DependencyObject CurrentHelpDO { get; set; }
private Popup CurrentHelpPopup { get; set; }
private bool HelpActive { get; set; }
private MouseEventHandler _helpHandler = null;
private readonly OuterGlowBitmapEffect YellowGlow =
        new OuterGlowBitmapEffect() 
        { GlowColor = Colors.Yellow, GlowSize = 10, Noise = 1 };

Step 3 - F1 Key Causes Help Toggle

private void winMain_KeyDown(object sender, KeyEventArgs e)
{
    if (e.Key == Key.F1)
    {
        e.Handled = true;
        ToggleHelp();
    }
}

Step 4 - Recursively Toggle Help through the Visual Tree

private void ToggleHelp()
{
    // Turn the current help off
    CurrentHelpDO = null;
    if (CurrentHelpPopup != null)
    {
        CurrentHelpPopup.IsOpen = false;
    }

    // Toggle current state; add/remove mouse handler
    HelpActive = !HelpActive;

    if (_helpHandler == null)
    {
        _helpHandler = new MouseEventHandler(winMain_MouseMove);
    }

    if (HelpActive)
    {
        winMain.MouseMove += _helpHandler;
    }
    else
    {
        winMain.MouseMove -= _helpHandler;
    }

    // Start recursive toggle at visual root
    ToggleHelp(canvMain);
}

private void ToggleHelp(DependencyObject dependObj)
{
    // Continue recursive toggle. Using the VisualTreeHelper works nicely.
    for (int x = 0; x < VisualTreeHelper.GetChildrenCount(dependObj); x++)
    {
        DependencyObject child = VisualTreeHelper.GetChild(dependObj, x);
        ToggleHelp(child);
    }

    // BitmapEffect is defined on UIElement so our DependencyObject 
    // must be a UIElement also
    if (dependObj is UIElement)
    {
        UIElement element = (UIElement)dependObj;
        if (HelpActive)
        {
            string helpText = AutomationProperties.GetHelpText(element);
            if (!helpText.IsNothing())
            {
                // Any effect can be used, I chose a simple yellow highlight
                ((UIElement)element).BitmapEffect = YellowGlow;
            }
        }
        else if (element.BitmapEffect == YellowGlow)
        {
            element.BitmapEffect = null;
        }
    }
}

Step 5 - Track the Mouse

private void winMain_MouseMove(object sender, MouseEventArgs e)
{
    // You can check the HelpActive property if desired, however 
    // the listener should not be hooked up so this should not be firing
    HitTestResult hitTestResult = 
      VisualTreeHelper.HitTest(((Visual)sender), e.GetPosition(this));
    if (hitTestResult.VisualHit != null && 
            CurrentHelpDO != hitTestResult.VisualHit)
    {
        // Walk up the tree in case a parent element has help defined
        DependencyObject checkHelpDO = hitTestResult.VisualHit;
        string helpText = AutomationProperties.GetHelpText(checkHelpDO);
        while (helpText.IsNothing() && checkHelpDO != null 
               && checkHelpDO != canvMain && checkHelpDO != winMain)
        {
            checkHelpDO = VisualTreeHelper.GetParent(checkHelpDO);
            helpText = AutomationProperties.GetHelpText(checkHelpDO);
        }

        if (helpText.IsNothing() && CurrentHelpPopup != null)
        {
            CurrentHelpPopup.IsOpen = false;
            CurrentHelpDO = null;
        }
        else if (!helpText.IsNothing() && CurrentHelpDO != checkHelpDO)
        {
            CurrentHelpDO = checkHelpDO;
            // New visual "stack" hit, close old popup, if any
            if (CurrentHelpPopup != null)
            {
                CurrentHelpPopup.IsOpen = false;
            }

            // Obviously you can make the popup look anyway you want with
            // any number of options. I chose a simple tooltip look-and-feel.
            // (caching/reuse omitted for example)
            CurrentHelpPopup = new Popup()
            {
                AllowsTransparency = true,
                PopupAnimation = PopupAnimation.Scroll,
                PlacementTarget = (UIElement)hitTestResult.VisualHit,
                Child = new Border()
                {
                    CornerRadius = new CornerRadius(10),
                    BorderBrush = new SolidColorBrush(Colors.Goldenrod),
                      BorderThickness = new Thickness(2),
                      Background = new SolidColorBrush(Colors.LightYellow),
                      Child = new TextBlock()
                      {
                        Margin = new Thickness(10),
                        Text = helpText.Replace("\\r\\n", "\r\n"),
                        FontSize = 14,
                        FontWeight = FontWeights.Normal
                      }
                }
            };
            CurrentHelpPopup.IsOpen = true;
        }
    }
}

Update - 2/28/10

One of the things I didn't like about this solution was that traversing the visual tree did not seem very WPF'ish. After Pete O'Hanlon reminded me about the built-in ApplicationCommand.Help command, I took another look and came up with a different approach. It's rather simple, you just create a value converter that converts the boolean HelpActive property (which is now a DependencyProperty) to BitmapEffect. If help is active, you return the yellow glow object; otherwise just null. The only issue then is that you have to bind the BitmapEffect property of each control that has help set. This can be done individually or on a per-Style basis. If anyone knows a way to link the BitmapEffect and HelpText properties so you only have to set the help, please let me know.

[ValueConversion(typeof(bool), typeof(BitmapEffect))]
public class GlowConverter : IValueConverter
{
    private OuterGlowBitmapEffect _glow = null;

    public GlowConverter()
    {
        _glow = new OuterGlowBitmapEffect() 
              { GlowSize = 10, Noise=1, GlowColor = Colors.Yellow };
    }

    public object Convert(object value, Type targetType, 
                  object parameter, CultureInfo culture)
    {
        return ((bool)value) ? _glow : null;
    }

    public object ConvertBack(object value, Type targetType, 
                  object parameter, CultureInfo culture)
    {
        return value != null;
    }
}

-----------

public partial class Window1 : Window
{
    public static readonly DependencyProperty HelpActiveProperty =
    DependencyProperty.Register("HelpActive", typeof(bool), typeof(Window1), 
       new FrameworkPropertyMetadata(false,
           FrameworkPropertyMetadataOptions.AffectsRender));

    public bool HelpActive
    {
        get
        {
            return (bool)GetValue(HelpActiveProperty);
        }
        set
        {
            SetValue(HelpActiveProperty, value);
        }
    }

    public Window1()
    {
        InitializeComponent();

        CommandBindings.Add(new CommandBinding(ApplicationCommands.Help,
            (x, y) => HelpActive = !HelpActive,
            (x, y) => y.CanExecute = true));
    }

}
App.xaml
<gui:GlowConverter x:Key="GlowChange" />

<Style TargetType="Button">
    <Setter Property="Background" Value="Red" />
    <Setter Property="BitmapEffect"
            Value="{Binding Source={x:Static win:Application.Current},
        Path=MainWindow.HelpActive, Converter={StaticResource GlowChange}}" />
</Style>

Conclusion

Despite the fact that there is no built-in help system in WPF, I was pleasantly surprised to discover how easy it was to create one. I will be the first to admit this solution is not perfect, but I feel it is a good fit for a lot of different applications.

Update History

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here