Home > Windows Presentation Foundation > Easy help with WPF

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.

  1. May 2, 2009 at 3:38 pm | #1

    I love that one! Thanks for the sample :-)

    • peteohanlon
      May 2, 2009 at 7:52 pm | #2

      That’s the fuel that keeps me going Philipp. Glad you like it.

  2. May 2, 2009 at 4:09 pm | #3

    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. May 2, 2009 at 4:09 pm | #4

    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

    • peteohanlon
      May 2, 2009 at 7:51 pm | #5

      Cheers Karl. You could pass the keyword and filename for a one off topic.

  4. Walt Ritscher
    May 2, 2009 at 10:48 pm | #6

    This is really nice. Thanks for sharing.

    • peteohanlon
      May 3, 2009 at 7:39 pm | #7

      Thanks Walt. I like this one.

  5. May 25, 2009 at 6:03 am | #8

    Great, thanks!

  6. May 28, 2009 at 7:18 pm | #9

    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

    • peteohanlon
      May 28, 2009 at 7:29 pm | #10

      Mark, I love the extensibility of WPF. It’s just so cool.

  7. thomas
    May 29, 2009 at 10:49 am | #11

    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 :)

  8. micaleel
    June 16, 2009 at 10:01 pm | #12

    Thanks, this has been very useful.

    • peteohanlon
      June 17, 2009 at 9:14 am | #13

      I’m glad it’s been of help to you. That’s the music that keeps me blogging.

  9. June 17, 2009 at 9:56 am | #14

    Thanks, this has been very helpful.

    • micaleel
      June 17, 2009 at 10:03 am | #15

      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.

  10. July 26, 2009 at 5:31 am | #16

    Nice! Very very nice!!

  11. Peter M
    September 6, 2009 at 8:06 pm | #17

    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.

    • peteohanlon
      September 6, 2009 at 8:15 pm | #18

      @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.

  12. September 8, 2009 at 12:57 pm | #19

    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.

  13. September 8, 2009 at 1:21 pm | #20

    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)

  14. September 8, 2009 at 1:23 pm | #21

    Damn comment software swallowed my XAML. But yes I have a reasonably configured system with the Help file defined in the Window tag

  15. October 13, 2009 at 11:19 am | #22

    Thank you for this piece of code, it was exactly what I needed! Keep up the good work, I’ll be reading your stuff from now on!

    • peteohanlon
      October 14, 2009 at 11:46 am | #23

      Thanks Tomas. I’m glad you like it.

  16. Matt R
    November 3, 2009 at 2:14 pm | #24

    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!

  17. November 7, 2009 at 8:53 am | #25

    Hi,

    great work, awarded you 5 sticky stars…

    • peteohanlon
      November 7, 2009 at 12:53 pm | #26

      Thanks for that – it feels gooey, in a good way;->

  1. November 7, 2009 at 7:37 am | #1