Easy help with WPF

If, like me, you like your applications to provide Context sensitive help, you’ve probably had a play around with the ApplicationCommands.Help command. In order to simplify hooking your help into your application, I’ve written the following code.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Input;
using form = System.Windows.Forms;
using System.Windows.Media;

namespace HelpProvider
{
  /// <summary>
  /// This class provides the ability to easily attach Help functionality 
  /// to Framework elements. To use it, you need to
  /// add a reference to the HelpProvider in your XAML. The FilenameProperty
  /// is used to specify the name of the helpfile, and the KeywordProperty specifies
  /// the keyword to be used with the search.
  /// </summary>
  /// <remarks>
  /// The FilenameProperty can be at a higher level of the visual tree than
  /// the KeywordProperty, so you don't need to set the filename each time.
  /// </remarks>
  public static class Help
  {
    /// <summary>
    /// Initialize a new instance of <see cref="Help"/>.
    /// </summary>
    static Help()
    {
      // Rather than having to manually associate the Help command, let's take care
      // of this here.
      CommandManager.RegisterClassCommandBinding(typeof(FrameworkElement),
        new CommandBinding(ApplicationCommands.Help,
          new ExecutedRoutedEventHandler(Executed),
          new CanExecuteRoutedEventHandler(CanExecute)));
    }

    #region Filename

    /// <summary>
    /// Filename Attached Dependency Property
    /// </summary>
    public static readonly DependencyProperty FilenameProperty =
      DependencyProperty.RegisterAttached("Filename", typeof(string), typeof(Help));

    /// <summary>
    /// Gets the Filename property.
    /// </summary>
    public static string GetFilename(DependencyObject d)
    {
      return (string)d.GetValue(FilenameProperty);
    }

    /// <summary>
    /// Sets the Filename property.
    /// </summary>
    public static void SetFilename(DependencyObject d, string value)
    {
      d.SetValue(FilenameProperty, value);
    }

    #endregion
    
    #region Keyword

    /// <summary>
    /// Keyword Attached Dependency Property
    /// </summary>
    public static readonly DependencyProperty KeywordProperty =
      DependencyProperty.RegisterAttached("Keyword", typeof(string), typeof(Help));

    /// <summary>
    /// Gets the Keyword property.
    /// </summary>
    public static string GetKeyword(DependencyObject d)
    {
      return (string)d.GetValue(KeywordProperty);
    }

    /// <summary>
    /// Sets the Keyword property.
    /// </summary>
    public static void SetKeyword(DependencyObject d, string value)
    {
      d.SetValue(KeywordProperty, value);
    }
    #endregion

    #region Helpers
    private static void CanExecute(object sender, CanExecuteRoutedEventArgs args)
    {
      FrameworkElement el = sender as FrameworkElement;
      if (el != null)
      {
        string fileName = FindFilename(el);
        if (!string.IsNullOrEmpty(fileName))
          args.CanExecute = true;
      }
    }

    private static void Executed(object sender, ExecutedRoutedEventArgs args)
    {
      // Call ShowHelp.
      DependencyObject parent = args.OriginalSource as DependencyObject;
      string keyword = GetKeyword(parent);
      if (!string.IsNullOrEmpty(keyword))
      {
        form.Help.ShowHelp(null, FindFilename(parent), keyword);
      }
      else
      {
        form.Help.ShowHelp(null, FindFilename(parent));
      }
    }

    private static string FindFilename(DependencyObject sender)
    {
      if (sender != null)
      {
        string fileName = GetFilename(sender);
        if (!string.IsNullOrEmpty(fileName))
          return fileName;
        return FindFilename(VisualTreeHelper.GetParent(sender));
      }
      return null;
    }
    #endregion

  }
}

Using it couldn’t be simpler, set up the Filename in your XAML and add any keywords you need to search on against your FrameworkElement items. The advantage of this approach is that you can bind different parts of your UI to different helpfiles if you want.

<Window 
  x:Class="HelpSample.Window1"
  xmlns="<a href="http://schemas.microsoft.com/winfx/2006/xaml/presentation">http://schemas.microsoft.com/winfx/2006/xaml/presentation</a>"
  xmlns:x="<a href="http://schemas.microsoft.com/winfx/2006/xaml">http://schemas.microsoft.com/winfx/2006/xaml</a>"
  Title="Window1" Height="300" Width="300"
  xmlns:help="clr-namespace:HelpProvider;assembly=HelpProvider"
  help:Help.Filename="MyHelpfile.chm"
  >
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition />
      <RowDefinition />
    </Grid.RowDefinitions>
    <TextBox help:Help.Keyword="MyKeyword" Grid.Row="0" Text="Keyword based search" />
    <TextBox Grid.Row="1" Text="No keyword" />
  </Grid>
</Window>

I hope this helps you as much, as it helps me.

61 thoughts on “Easy help with WPF

  1. Karl Shifflett

    Pete,

    Nice work sir! Like the way the help file can be scoped.

    I assume for a one off topic, the Filename and Keyword could be passed and used.

    See you soon Mate,

    Cheers,

    Karl

  2. Karl Shifflett

    Pete,

    Nice work sir! Like the way the help file can be scoped.

    I assume for a one off topic, the Filename and Keyword could be passed and used.

    See you soon Mate,

    Cheers,

    Karl

  3. This is a nice implementation – thanks for sharing it. It’s too bad WPF doesn’t have this built in frankly.. but isn’t it cool how you can bolt on additional features?

    Cheers,
    mark

  4. thomas

    looking for the filename in canexecute is a cpu and time consuming action.
    better to do that once per filename and then store it in a static variable.

    anyway…good solution 🙂

    1. micaleel

      Ooops, please disregard this comment. I totally forgot that I already made the coment. It shows you how truly helpful it was, hehe. Thanks anyway.

  5. Peter M

    Great code for help. It was exactly what I was looking for today.

    One question I have is how do you tie this system into a menu item? I tried setting up a menuitem with a command=”help” and this correctly invoked the Executed() method, but it threw an exception on:

    form.Help.ShowHelp(null, FindFilename(parent));

    It seems that FindFilename() continued to walk up the tree looking for a parent object with the correct Filename dependency property until it ran out of things to check.

    1. peteohanlon

      @Peter – thanks for the comment. I assume that you’ve hooked your menu item up to ApplicationCommands.Help – if so, this should work out of the box, as long as you’ve put the filename somewhere up the visual tree. In the sample, I hook the filename into the window definition.

  6. Pete,
    I had ignored that part of the program since I wrote the comment and now that I tested it again I find that it works – yet I have done nothing except (most likely) done a clean compile in between. I’ll chalk that one up to the PF gods not liking me this weekend.

  7. Pete .. actually I take that back .. I just encountered my error. I have:

    When I hit F1 the help comes up as desired. But if I navigate to the Help item in the menu and select it I get the exception I was referring too above:

    Type: System.ArgumentException
    Location: System.Windows.Forms.Help.ShowHTMLFile()
    Parameter: URL
    Message: Help URL ” is not valid

    From my understanding it is as if my menu item is not in the Visual Tree for some reason (and I don’t have any experience to know if it should or shouldn’t be there)

  8. Matt R

    This is great, thanks! Is there a way to have the sender react to a mouse over rather than a click or focused property? For example, I want to be able to hover over a button, press F1, and have that button be the sender. Thanks in advance for any advice that anyone has on this!

  9. Pingback: WPFGlue Patterns « The WPFGlue Blog

  10. Robert

    Nice idea, thanks.

    Instead of your recursive FindFilename function, I’d go with inheritable DependencyProperties, for both FileName and Keyword. Simpler code, and probably faster.

    I’d also be cautious doing that much in CanExecute.

  11. Chad

    This is a very nice solution — thank you!

    I’ve got it working for individual elements within a window (which was the aim I believe), but I would also like to set it for the entire window as well. In some of my windows I won’t have help for each element but just for the whole window. Am I missing how to do this?

    1. Chad

      I solved my question by creating another helper method called FindKeyword structured just like FindFilename which looks up the tree for a keyword. Pretty simple once I thought about it. Thanks again for the solution.

  12. Richard

    “using form = System.Windows.Forms;”

    Eeew! You’ve just loaded a 5Mb assembly to use one feature! That’s so *nasty*!

    Seriously though, if you’re only using CHM files, it’s not too hard to P/Invoke the HtmlHelp function from hhctrl.ocx to avoid the WinForms dependency.

    public enum NativeHtmlHelpCommand
    {
    Topic = 0,
    TableOfContents = 1,
    Index = 2,
    }

    [DllImport(“hhctrl.ocx”, CharSet = CharSet.Auto, BestFitMapping = false, ThrowOnUnmappableChar = true)]
    public static extern int HtmlHelp(
    HandleRef hwndCaller,
    [MarshalAs(UnmanagedType.LPTStr)] string pszFile,
    NativeHtmlHelpCommand uCommand,
    [MarshalAs(UnmanagedType.LPTStr)] string dwData);

  13. Thanks for the post, I like the direction, but some problems:

    Already very similar to the code posted here
    http://blogs.msdn.com/mikehillberg/archive/2007/07/26/a-context-sensitive-help-provider-in-wpf.aspx

    However, in that post the author does not have to make the recursive function call – rather you can just use the nature of routed events..
    “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.”

    Second, I totally agree with the last comment – however I blame WPF — is this really the case? That to show help files/chm in WPF, the best practice is to depend on System.Windows.Forms ??? That’s ridiculous.

  14. Christian LOUIS

    Nice article. But I cannot compile my project because I have an error for the line “help:Help.Filename=”MyHelpfile.chm” error : property Filename cannot be find in the type Help. Have I to add any thing or configure the project? What is wrong?

    Best regards

  15. Raghu

    Hi sir i am getting error in xaml file that Attachable Property Filename was not found in Help please help me i am new to WPF i got work to implement help system for our internal project

    Thanks
    Raghu

  16. Raghu

    Please teach me Step by step how to make this help feature work

    i dont even know how to create chm files where to store to invoke all these things

    please help me

    Regards
    Raghu

  17. Raghu

    ok i sort out this problem too but the page corresponds to keyword of text box is not displaying.Its showing error This program cannot display the webpage.

    Thanks
    Raghu

  18. A123

    hi, i’m having the same problem i can’t find the attached properties filename and keyword when trying to add this to my xaml, can u tell me how you solved it?

  19. Jake

    2 years after, your code is still helping people (like me!).

    My only suggested improvement would probably be, able to go directly to a specific page.. or maybe I haven’t found out how to do that yet using your sample.

    Thanks a lot for this!

  20. Boopathi

    Your code is very nice. But it is not working while passing ‘Keyword” to help menthod.for this case chm file was opened but not selecting given keyword topic.

    Ex :
    form.Help.ShowHelp(null, FindFilename(parent), form.HelpNavigator.Topic, (Object)keyword);

    Please correct me.

    Advance Thanks…

  21. Bing

    This example works great for us.

    The question I have is how can I give a path to “MyHelpfile.chm”. It works if I hard code the path in help:Help.Filename, but I want to use a “global” variable to store the path of MyHelpfile.chm”

    Thanks

    1. peteohanlon

      Bing – the easiest way to achieve this would be to just modify the codebase to fall back to that variable if the filename wasn’t set. So, if you got null out of FindFilename, just go to your variable.

  22. canatan

    is there anyway we can use the stuff in this class and bind it to a button in mvvm ? so that when you click the button the help file is displayed?

  23. LWDaddio

    I’m trying to use this but running into problems. I’m using WPF with .NET 4.0. I have the declarations in the Window and with that using F1 brings up the .chm file just fine. But if I try to add help to a menuitem, I have to also specify the filename or it doesn’t find it and throws an exception. And if I try to use a keyword, the .chm opens at the Index tab, but the keyword field is blank and so it doesn’t show the indexed page. Any idea what I might be doing wrong here? Thanks!

  24. Pingback: My Pocket reads (readitlater) « Opposable Bits

  25. Abhishek S :
    @Boopathi
    set Keyword to the URL path of HTM/page file of CHM app.

    Hi
    I have an chm file made with Help&Manual.
    What path should I use for KeywordProperty ?
    I can view the help file but I can’t use Keyword (it’s showing a blank topic with an not found error).
    Thank’s
    Sorin

  26. Hi Pete – 6 years on and your code is still being used 🙂 I too am having problems with the Keyword part of things. I’ve added keywords to the htm files and successfully run ‘Test/Keyword lookup…’ in help workshop, and tested that the keyword is correctly being passed to the Executed method, but on hitting F1, the help viewer opens with ‘This page can’t be displayed’ and asks me to ensure the address: //ieframe.dll/dnserrordiagoff.htm# is correct.

    Is there a need to prepend the htm page as Abhishek S suggests, or am i doing something else wrong?

  27. OIC, your solution doesn’t use the “Keywords” that HTML Help Workshop refers to; with Easy Help, you set the Keyword attached property to the name of the htm file (including extension). So you don’t have (for eg) 1 htm file per application window with multiple Keywords, you have one htm file per help topic.

    Many thanks for the code Pete. Got it now. 🙂

  28. clyde

    Abhishek, mcalex,

    Thanks for your comments. Was having problems with “Keywords” also. Just want to add that in my .chm file I’m storing .htm pages in a separate folder “html.” To expand on mcalex’s answer, I needed to add the following to my XAML to get Easy Help to jump to the right page:

    help:Help.Keywork=”html\IntroPage.htm”

    Backslash was needed. Forward slash did not work. “.htm” extension needed as well.

    And, Pete, let me add my “thank-you” to the others.

Leave a comment