A Context-Sensitive Help Provider in Wpf

Here's an example of a way to add context-sensitive help to your application. 

The main idea is to simply use the built-in ApplicationCommands.Help command.  This command is already tied to the F1 key, and so executes when you hit F1, and tells your command handler what element the user was on when it was hit.

First, start with this little application:

<Window x:Class="ContextSensitiveHelp.Window1"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    Title="Window1" Height="300" Width="300" >

  <Grid>

    <Grid.ColumnDefinitions>

      <ColumnDefinition Width='auto'/>

      <ColumnDefinition Width='*'/>

    </Grid.ColumnDefinitions>

    <Grid.RowDefinitions>

      <RowDefinition Height='auto'/>

      <RowDefinition Height='auto' />

    </Grid.RowDefinitions>

    <TextBlock Grid.Column='0' Grid.Row='0' >Name:</TextBlock>

    <TextBlock Grid.Column='0' Grid.Row='1' >Address</TextBlock>

    <TextBox Grid.Column='1' Grid.Row='0' Name='NameField' />

    <TextBox Grid.Column='1' Grid.Row='1' Name='AddressField' />

  </Grid>

</Window>

... which looks like this:

image

Now add some help text.  I'll add specific help text to the NameField text box, and general help text to the Window, but nothing to the AddressField, just to show how that behaves:

<Window x:Class="ContextSensitiveHelp.Window1"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    Title="Window1" Height="300" Width="300"

    xmlns:h="clr-namespace:ContextSensitiveHelp"

    h:HelpProvider.HelpString='This form is for your name and address'  >

  <Grid>

    <Grid.ColumnDefinitions>

      <ColumnDefinition Width='auto'/>

      <ColumnDefinition Width='*'/>

    </Grid.ColumnDefinitions>

    <Grid.RowDefinitions>

      <RowDefinition Height='auto'/>

      <RowDefinition Height='auto' />

    </Grid.RowDefinitions>

    <TextBlock Grid.Column='0' Grid.Row='0' >Name:</TextBlock>

    <TextBlock Grid.Column='0' Grid.Row='1' >Address</TextBlock>

    <TextBox Grid.Column='1' Grid.Row='0' Name='NameField'

               h:HelpProvider.HelpString="Enter your name here"/>

    <TextBox Grid.Column='1' Grid.Row='1' Name='AddressField' />

  </Grid>

</Window>

So you see here that the help text is being provided by attached properties defined by a class named HelpProvider.  Here's the attached property definition:

static class HelpProvider

{

    public static string GetHelpString(DependencyObject obj)

    {

        return (string)obj.GetValue(HelpStringProperty);

    }

    public static void SetHelpString(DependencyObject obj, string value)

    {

        obj.SetValue(HelpStringProperty, value);

    }

    public static readonly DependencyProperty HelpStringProperty =

        DependencyProperty.RegisterAttached(

                       "HelpString", typeof(string), typeof(HelpProvider));

}

I made this a help string property, but you could have a HelpKeyword property that held a help topic ID, or a HelpURI property that referenced online documentation, etc.

Now we just need to respond to the command.  The way you respond to commands is with event listeners registered to handle the command.  You can simply set up a command handler on a particular element by setting a CommandBinding like this:

<Window x:Class="ContextSensitiveHelp.Window1"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    Title="Window1" Height="300" Width="300"

    xmlns:h="clr-namespace:ContextSensitiveHelp"

    h:HelpProvider.HelpString='This form is for your name and address' >

  <Window.CommandBindings>

    <CommandBinding Command='ApplicationCommands.Help' Executed='Window_Help_Executed' />

  </Window.CommandBindings>

  <Grid>

    <Grid.ColumnDefinitions>

      <ColumnDefinition Width='auto'/>

      <ColumnDefinition Width='*'/>

    </Grid.ColumnDefinitions>

    <Grid.RowDefinitions>

      <RowDefinition Height='auto'/>

      <RowDefinition Height='auto' />

    </Grid.RowDefinitions>

    <TextBlock Grid.Column='0' Grid.Row='0' >Name:</TextBlock>

    <TextBlock Grid.Column='0' Grid.Row='1' >Address</TextBlock>

   <TextBox Grid.Column='1' Grid.Row='0' Name='NameField'

               h:HelpProvider.HelpString="Enter your name here"/>

    <TextBox Grid.Column='1' Grid.Row='1' Name='AddressField' />

  </Grid>

</Window>

... and then respond to the command like this:

private void Window_Help_Executed(object sender, ExecutedRoutedEventArgs e)

{

    FrameworkElement source = e.Source as FrameworkElement;

    if (source != null)

    {

        string helpString = HelpProvider.GetHelpString(source);

        if (helpString != null)

        {

            System.Windows.MessageBox.Show("Help: " + helpString);

        }

    }

}

The problem with that is that you'd really rather not implement the handling as part of your application, but rather as part of the HelpProvider (following the WinForms model).  Also, the code above finds the HelpString on NameField, which has a HelpString set, but it doesn't find anything for the AddressField, because it doesn't have a good way to find the more general HelpString set on the Window.

So instead of putting the command handler in the Window, we'll put it in the HelpProvider itself.  We can do this by registering a command handler directly against the FrameworkElement class; now we'll get called for any FrameworkElement instance in the application.  We'll also register for the command's CanExecute handler.  This allows us to decide if we should handle a command at an element, or let it bubble up to a higher handler.  That is, this will allow the Help command to bubble up from AddressField, which has no HelpString, to Window which does.

The HelpProvider class is then updated with these new members:

static class HelpProvider

{

    static HelpProvider()

    {

        CommandManager.RegisterClassCommandBinding(

            typeof(FrameworkElement),

            new CommandBinding(

                ApplicationCommands.Help,

                new ExecutedRoutedEventHandler(Executed),

                new CanExecuteRoutedEventHandler(CanExecute)));

    }

    static private void CanExecute(object sender, CanExecuteRoutedEventArgs e)

    {

        FrameworkElement senderElement = sender as FrameworkElement;

        if (HelpProvider.GetHelpString(senderElement) != null)

            e.CanExecute = true;

    }

    static private void Executed(object sender, ExecutedRoutedEventArgs e)

    {

        MessageBox.Show("Help: " + HelpProvider.GetHelpString(sender as FrameworkElement));

    }

    ...

... and we can remove the CommandBindings from the Window:

<Window x:Class="ContextSensitiveHelp.Window1"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    Title="Window1" Height="300" Width="300"

    xmlns:h="clr-namespace:ContextSensitiveHelp"

    h:HelpProvider.HelpString='This form is for your name and address' >

  <Window.CommandBindings>

    <CommandBinding Command='ApplicationCommands.Help' Executed='Window_Help_Executed' />

  </Window.CommandBindings>

  <Grid>

    <Grid.ColumnDefinitions>

      <ColumnDefinition Width='auto'/>

      <ColumnDefinition Width='*'/>

    </Grid.ColumnDefinitions>

    <Grid.RowDefinitions>

      <RowDefinition Height='auto'/>

      <RowDefinition Height='auto' />

    </Grid.RowDefinitions>

    <TextBlock Grid.Column='0' Grid.Row='0' >Name:</TextBlock>

    <TextBlock Grid.Column='0' Grid.Row='1' >Address</TextBlock>

    <TextBox Grid.Column='1' Grid.Row='0' Name='NameField'

               h:HelpProvider.HelpString="Enter your name here"/>

    <TextBox Grid.Column='1' Grid.Row='1' Name='AddressField' />

  </Grid>

</Window>

... and now F1 on NameField displays "Enter your name here", and F1 on the AddressField displays "This form is for your name and address".

Finally, note that you can also do more exciting things in the help handler, like load a chm file with the System.Windows.Forms.Help class:

static private void Executed(object sender, ExecutedRoutedEventArgs e)

{

    System.Windows.Forms.Help.ShowHelp(null, @"MyHelp.chm");

}

 

ContextSensitiveHelp.zip