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=http://schemas.microsoft.com/winfx/2006/xaml/presentation


    xmlns:x=http://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=http://schemas.microsoft.com/winfx/2006/xaml/presentation


    xmlns:x=http://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=http://schemas.microsoft.com/winfx/2006/xaml/presentation


    xmlns:x=http://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=http://schemas.microsoft.com/winfx/2006/xaml/presentation


    xmlns:x=http://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

Comments (20)

  1. Rudi Grobler says:

    Dr Tim Sneath gives a overview of what is new in WPF 3.5 http://blogs.msdn.com/tims/archive/2007/07/27

  2. andemann says:

    Any ideas on how to make this appear based on mouseover and not just focus?

    Attempting to create office 2007 style super tooltips with the F1 for more help.

    Andreas

  3. andemann says:

    I found the answer. Use CommandBinding in the custom toolTip contructor.

    Andreas

  4. black_nm says:

    I’ve created a full code for HelpProvider:

    using System.Windows;

    using System.Windows.Forms;

    using System.Windows.Input;

    using System.Windows.Media;

    namespace Diginesis.Presentation.Controls

    {

       public static class DiginesisHelpProvider

       {

           #region Fields

           private static string mHelpNamespace = null;

           private static bool mShowHelp = false;

           private static string mNotFoundTopic;

           public static readonly DependencyProperty HelpKeywordProperty =

               DependencyProperty.RegisterAttached("HelpKeyword", typeof (string), typeof (DiginesisHelpProvider));

           public static readonly DependencyProperty HelpNavigatorProperty =

               DependencyProperty.RegisterAttached("HelpNavigator", typeof (HelpNavigator), typeof (DiginesisHelpProvider),

                                                   new PropertyMetadata(HelpNavigator.TableOfContents));

           public static readonly DependencyProperty ShowHelpProperty =

               DependencyProperty.RegisterAttached("ShowHelp", typeof (bool), typeof (DiginesisHelpProvider));

           #endregion

           #region Constructors

           static DiginesisHelpProvider()

           {

               CommandManager.RegisterClassCommandBinding(

                   typeof (FrameworkElement),

                   new CommandBinding(ApplicationCommands.Help, OnHelpExecuted, OnHelpCanExecute));

           }

           #endregion

           #region Public Properties

           public static string HelpNamespace

           {

               get { return mHelpNamespace; }

               set { mHelpNamespace = value; }

           }

           public static bool ShowHelp

           {

               get { return mShowHelp; }

               set { mShowHelp = value; }

           }

           public static string NotFoundTopic

           {

               get { return mNotFoundTopic; }

               set { mNotFoundTopic = value; }

           }

           #endregion

           #region Public Methods

           #region HelpKeyword

           public static string GetHelpKeyword(DependencyObject obj)

           {

               return (string) obj.GetValue(HelpKeywordProperty);

           }

           public static void SetHelpKeyword(DependencyObject obj, string value)

           {

               obj.SetValue(HelpKeywordProperty, value);

           }

           #endregion

           #region HelpNavigator

           public static HelpNavigator GetHelpNavigator(DependencyObject obj)

           {

               return (HelpNavigator) obj.GetValue(HelpNavigatorProperty);

           }

           public static void SetHelpNavigator(DependencyObject obj, HelpNavigator value)

           {

               obj.SetValue(HelpNavigatorProperty, value);

           }

           #endregion

           #region ShowHelp

           public static bool GetShowHelp(DependencyObject obj)

           {

               return (bool) obj.GetValue(ShowHelpProperty);

           }

           public static void SetShowHelp(DependencyObject obj, bool value)

           {

               obj.SetValue(ShowHelpProperty, value);

           }

           #endregion

           #endregion

           #region Private Members

           private static void OnHelpCanExecute(object sender, CanExecuteRoutedEventArgs e)

           {

               e.CanExecute = CanExecuteHelp((DependencyObject) sender) || ShowHelp;

           }

           private static bool CanExecuteHelp(DependencyObject sender)

           {

               if (sender != null)

               {

                   if (GetShowHelp(sender))

                       return true;

                   return CanExecuteHelp(VisualTreeHelper.GetParent(sender));

               }

               return false;

           }

           private static DependencyObject GetHelp(DependencyObject sender)

           {

               if (sender != null)

               {

                   if (GetShowHelp(sender))

                       return sender;

                   return GetHelp(VisualTreeHelper.GetParent(sender));

               }

               return null;

           }

           private static void OnHelpExecuted(object sender, ExecutedRoutedEventArgs e)

           {

               DependencyObject ctl = GetHelp(sender as DependencyObject);

               if (ctl != null && GetShowHelp(ctl))

               {

                   string parameter = GetHelpKeyword(ctl);

                   HelpNavigator command = GetHelpNavigator(ctl);

                   if (!e.Handled && !string.IsNullOrEmpty(HelpNamespace))

                   {

                       if (!string.IsNullOrEmpty(parameter))

                       {

                           Help.ShowHelp(null, HelpNamespace, command, parameter);

                           e.Handled = true;

                       }

                       if (!e.Handled)

                       {

                           Help.ShowHelp(null, HelpNamespace, command);

                           e.Handled = true;

                       }

                   }

                   if (!e.Handled && !string.IsNullOrEmpty(HelpNamespace))

                   {

                       Help.ShowHelp(null, HelpNamespace);

                       e.Handled = true;

                   }

               }

               else if (ShowHelp)

               {

                   if (!string.IsNullOrEmpty(NotFoundTopic))

                       Help.ShowHelp(null, HelpNamespace, NotFoundTopic);

                   else

                       Help.ShowHelp(null, HelpNamespace);

                   e.Handled = true;

               }

           }

           #endregion

       }

    }

  5. black_nm says:

    How to use it!?!?!

    Add this to the App startup or constructor

    DiginesisHelpProvider.HelpNamespace = "Help.chm";

               DiginesisHelpProvider.ShowHelp = true;

    And set on a control the properties:

    c:DiginesisHelpProvider.ShowHelp="True"

                      c:DiginesisHelpProvider.HelpKeyword="HelpTest2.htm"

                      c:DiginesisHelpProvider.HelpNavigator="Topic"

  6. &#160; WPF Apps Jaime Rodriguez&#39;s list of WPF in Line-of-Business case studies CodePlex Project:

  7. WPF Apps Jaime Rodriguez&#39;s list of WPF in Line-of-Business case studies CodePlex Project: Slick Code

  8. kyawtun4biz says:

    clr-namespace:ContextSensitiveHelp not available in my WPF project.

    Do I need to added some reference assembly?

  9. Sorry, ContextSensitiveHelp is the namespace that the HelpProvider class lives in.  E.g.

    namespace ContextSensitiveHelp

    {

       static class HelpProvider

       {

           …

       }

    }

  10. NicolBlog says:

    I found thie following interesting post where is well described how to provide your windows presentation

  11. nightwriter says:

    I think something’s missing from the first example and the below would help it work.

    <TextBox.CommandBindings><CommandBinding Command=’ApplicationCommands.Help’ Executed=’Window_Help_Executed’ /></TextBox.CommandBindings>

    [[This is covered by the addition of the CommandBinding in the second example -Mike]]

     

  12. tigerprawn says:

    Hi Mike (or anyone that can help),

    I have to implement context-sensitive help using WPF for a new project I’m involved with at work and your code looks a perfect solution.

    However I spotted something when I was stepping through the code in debug. When the ‘HelpStringProperty’ dependency property is created and after the command binding is created in the constructor, if you then drill down to inspect the various attributes of the property, it seems to throw InvalidOperationExceptions against both ‘OwnerType’ and ‘PropertyType’ with the message ‘Method may only be called on a type for which Type.IsGenericParameters is true’.

    The code works fine even with the exceptions, but I’d like to understand why this is happening and try and resolve it if possible.

    Hope this all makes sense!

    Regards,

    Neil

  13. You’ll see this too if you do something simple in the debugger like

       typeof(Button).GenericParameterAttributes

    What’s happening is you’re making an attempt here to look at a property on System.Type that isn’t relevant to non-generic types.

  14. maheshan says:

    Hello Mike,

    I’ve downloaded the ContextSensitiveHelp.zip and was able to execute it. But I need the code in VB.NET version(I actually converted the code to VB.NET). I am not able to refer the HelpProvider.HelpString in my xaml code.

    Any help is appreciated.

  15. Here’s a VB version of HelpProvider.cs:

    Imports System

    Imports System.Collections.Generic

    Imports System.Linq

    Imports System.Text

    Imports System.Windows

    Imports System.Windows.Input

    Module HelpProvider

       Sub New()

           CommandManager.RegisterClassCommandBinding(GetType(FrameworkElement), New CommandBinding(ApplicationCommands.Help, New ExecutedRoutedEventHandler(AddressOf Executed), New CanExecuteRoutedEventHandler(AddressOf CanExecute)))

       End Sub

       Private Sub CanExecute(ByVal sender As Object, ByVal e As CanExecuteRoutedEventArgs)

           Dim senderElement As FrameworkElement = TryCast(sender, FrameworkElement)

           If HelpProvider.GetHelpString(senderElement) IsNot Nothing Then

               e.CanExecute = True

           End If

       End Sub

       Private Sub Executed(ByVal sender As Object, ByVal e As ExecutedRoutedEventArgs)

           ‘Help.ShowHelp(null, @"E:WindowsIMEimekr8helpimkr.chm");

           System.Windows.MessageBox.Show("Help: " & HelpProvider.GetHelpString(TryCast(sender, FrameworkElement)))

       End Sub

       Public Function GetHelpString(ByVal obj As DependencyObject) As String

           Return DirectCast(obj.GetValue(HelpStringProperty), String)

       End Function

       Public Sub SetHelpString(ByVal obj As DependencyObject, ByVal value As String)

           obj.SetValue(HelpStringProperty, value)

       End Sub

       Public ReadOnly HelpStringProperty As DependencyProperty = DependencyProperty.RegisterAttached("HelpString", GetType(String), GetType(HelpProvider))

    End Module

  16. Sadha says:

    For the program by MikeHillberg, how do I goto the desired topic in the chm file by pressing the F1 key from my wpf application?

  17. To go to a particular topic in a chm file, the Help.ShowHelp method has some overloads that let you select a specific topic.

  18. Pete O'Hanlon says:

    Mike – I can't believe I've only just found this post, you could have saved me a lot of time when I wrote my own version. 10 out of 10 from me, and a kick to myself that I didn't search further.

  19. Julie6 says:

    This is a nice feature we are looking for. I replaced the HelpString with KeyWord. static private void Executed(object sender, ExecutedRoutedEventArgs e)

           {

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

               System.Windows.Forms.Help.ShowHelp(null, @"MyHelp.chm", HelpProvider.GetKeyword(sender as FrameworkElement));

           }

    When I press F1, the chm was opened,but the right panel shows that "Page can't be displayed". Any help is appreciated.

  20. Dario says:

    Hi,

    This is very helpfull. Is there a possibility to use a non-static class? If yes, could you provide some example?