Click here to Skip to main content
15,861,125 members
Articles / Desktop Programming / WPF

A Simple, Integrated WPF Help System

Rate me:
Please Sign up or sign in to vote.
4.97/5 (16 votes)
1 Mar 2010CPOL5 min read 88.2K   1.1K   57   24
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

XML
<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

C#
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

C#
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

C#
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

C#
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.

C#
[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
XML
<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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
United States United States
Expert in C#, .NET, WinUI/WPF, Azure, and SQL Server.
I started working with .NET and C# professionally in 2003 and never looked back. I have been an integral part of a dozen complete software lifecycles, been a driving force behind two successful startups, and have led development teams.

Comments and Discussions

 
QuestionSimpler Solution.... Pin
jogibear998816-Sep-13 12:09
jogibear998816-Sep-13 12:09 
AnswerRe: Simpler Solution.... Pin
rcbapb225-Jan-15 1:14
rcbapb225-Jan-15 1:14 
GeneralRe: Simpler Solution.... Pin
rcbapb229-Jan-15 2:31
rcbapb229-Jan-15 2:31 
GeneralVery interesting feature Pin
Bjorn Backlund10-Nov-10 5:01
Bjorn Backlund10-Nov-10 5:01 
GeneralIf anyone is using .Net 4 the Glow effect will no longer work Pin
gardnerp11-Oct-10 8:36
gardnerp11-Oct-10 8:36 
GeneralRe: If anyone is using .Net 4 the Glow effect will no longer work Pin
Josh Fischer11-Oct-10 11:42
Josh Fischer11-Oct-10 11:42 
GeneralRe: If anyone is using .Net 4 the Glow effect will no longer work Pin
Sheridan1uk21-Oct-10 11:40
Sheridan1uk21-Oct-10 11:40 
QuestionCan it be used for all controls? Pin
Subrahmanya Narayana10-Aug-10 3:59
Subrahmanya Narayana10-Aug-10 3:59 
AnswerRe: Can it be used for all controls? Pin
Josh Fischer12-Aug-10 5:09
Josh Fischer12-Aug-10 5:09 
GeneralBrilliant! 5/5 Pin
John Adams4-Aug-10 6:02
John Adams4-Aug-10 6:02 
GeneralRe: Brilliant! 5/5 Pin
Josh Fischer4-Aug-10 12:16
Josh Fischer4-Aug-10 12:16 
GeneralVery nice Pin
Pete O'Hanlon19-Feb-10 6:12
subeditorPete O'Hanlon19-Feb-10 6:12 
GeneralRe: Very nice Pin
Josh Fischer22-Feb-10 2:38
Josh Fischer22-Feb-10 2:38 
GeneralRe: Very nice Pin
Pete O'Hanlon22-Feb-10 12:23
subeditorPete O'Hanlon22-Feb-10 12:23 
GeneralGood job, but have you also seen this Pin
Sacha Barber17-Feb-10 20:08
Sacha Barber17-Feb-10 20:08 
GeneralRe: Good job, but have you also seen this Pin
Josh Fischer18-Feb-10 3:31
Josh Fischer18-Feb-10 3:31 
GeneralRe: Good job, but have you also seen this Pin
Sacha Barber18-Feb-10 4:19
Sacha Barber18-Feb-10 4:19 
GeneralNice Pin
Marcelo Ricardo de Oliveira17-Feb-10 11:19
mvaMarcelo Ricardo de Oliveira17-Feb-10 11:19 
GeneralRe: Nice Pin
Josh Fischer17-Feb-10 14:33
Josh Fischer17-Feb-10 14:33 
QuestionWinforms Help System Pin
Anthony Daly17-Feb-10 11:18
Anthony Daly17-Feb-10 11:18 
AnswerRe: Winforms Help System Pin
Josh Fischer17-Feb-10 14:31
Josh Fischer17-Feb-10 14:31 
The algorithm, keyboard event, and mouse tracking would be virtually the same. Any visual elements, however, would be completely different.
It's been a while since I've done any WinForms development, but I would start by using the Tag property of the controls for the help text, and the Form.Controls property to start walking control tree. The Form.GetChildAtPoint method hopefully should mimic WPF's hit test features Confused | :confused: .
As for the visual effect to use, you may be able to just change the BackColor of each control with help text, but you'll have to look into that yourself.
Hope that helps.
Josh Fischer

AnswerRe: Winforms Help System Pin
Sacha Barber17-Feb-10 20:05
Sacha Barber17-Feb-10 20:05 
GeneralRe: Winforms Help System Pin
Anthony Daly18-Feb-10 0:22
Anthony Daly18-Feb-10 0:22 
GeneralRe: Winforms Help System Pin
Sacha Barber18-Feb-10 0:25
Sacha Barber18-Feb-10 0:25 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.