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:
- 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.
- 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.
- 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.
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 BitmapEffect
s 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;
}
}
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()
{
CurrentHelpDO = null;
if (CurrentHelpPopup != null)
{
CurrentHelpPopup.IsOpen = false;
}
HelpActive = !HelpActive;
if (_helpHandler == null)
{
_helpHandler = new MouseEventHandler(winMain_MouseMove);
}
if (HelpActive)
{
winMain.MouseMove += _helpHandler;
}
else
{
winMain.MouseMove -= _helpHandler;
}
ToggleHelp(canvMain);
}
private void ToggleHelp(DependencyObject dependObj)
{
for (int x = 0; x < VisualTreeHelper.GetChildrenCount(dependObj); x++)
{
DependencyObject child = VisualTreeHelper.GetChild(dependObj, x);
ToggleHelp(child);
}
if (dependObj is UIElement)
{
UIElement element = (UIElement)dependObj;
if (HelpActive)
{
string helpText = AutomationProperties.GetHelpText(element);
if (!helpText.IsNothing())
{
((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)
{
HitTestResult hitTestResult =
VisualTreeHelper.HitTest(((Visual)sender), e.GetPosition(this));
if (hitTestResult.VisualHit != null &&
CurrentHelpDO != hitTestResult.VisualHit)
{
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;
if (CurrentHelpPopup != null)
{
CurrentHelpPopup.IsOpen = false;
}
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;
}
}
}
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