Wednesday, August 29, 2007

Context Help Made Easy - Reloaded

Yet another issue

A month ago I've written about context sensitive help. The article can be found here and please refer to it before further reading.

Although the concept works pretty well, there are some obvious drawbacks which apply also to the original concept of Help Providers. What is the drawback?

Well, you are allowed to assign one and only one help topic to a single control.

Suppose however that some controls are "data-polymorphic" in a sense that they store completely different data in different contexts. Three examples:

  • trees usually keep different node types at different levels of their hierarchies
  • lists, listviews etc. often show different types of objects when initialized in different contexts
  • some generic forms are initialized with different types of data but behave in the same way each time

Every time such situation occurs there is a problem with binding a proper help topic to such control. Ideally, since the context is determined by the data, you would like to have something like that in the application code:

   1: string ProvideHelpTopicForControl( Control TheControl )
   2: {
   3:   if ( TheControl == treeViewStructure )
   4:      return ProvideHelpTopicForTreeView( treeViewStructure );
   5:   if ( TheControl == lstListViewWithItems )
   6:      return ProvideHelpTopicForListView( lstListViewWithItems );
   7: }
   9: ...
  11: string ProvideHelpTopicForTreeView( TreeView Tree )
  12: {
  13:   if ( Tree.SelectedNode != null )
  14:   {
  15:     switch ( Tree.SelectedNode.Text )
  16:     ...
  18:     or
  20:     switch ( Tree.SelectedNode.GetType().Name )
  21:     ...
  22:   }
  23:   else
  24:     return someDefaultMapping;
  25: }

(the code is not complete but I hope you get the idea)

While such solution could be perfect for a small application with just few different forms, it could be extremely tedious to implement and maintain it in a complex code.

And of cource, such hardcoded mapping would completely ruin the whole idea of moving the topic mapping out of the application!

Solution - additional context binding

I've spent few hours on thinking of a solution which would just extend the existing idea with features powerful enough to handle such "data-polymorphic" controls.

What I finally came up with can be summarized as follows:

  • each topic mapping gets another set of so-called binding contexts (no relation to data-binding!)
  • each binding context is identified by its name and holds complete description of help topic (including HelpKeyword and HelpNavigator)
  • when a topic mapping is found for a control (but before the topic is shown to the user), the engine tries to locate a binding context which could be more precise for selected control than the parent topic. If such binding context is found - it is used to show the help topic instead of the parent mapping.

Binding contexts are build in a different way for different types of controls but it is still done outside of the application code. In my implementation I handle just few different types of controls and leave the default implementation for other controls (the context is retrieved from the control's text).

   1: public static string[] GetBindingContext( Control Control )
   2: {
   3:     /* obsługa szczególnych typów formantów */
   5:     #region TreeView
   6:     if ( Control is ListView )
   7:     {
   8:         ListView l = Control as ListView;
   9:         if ( l.SelectedItems.Count > 0 &&
  10:              l.SelectedItems[0].Tag != null
  11:             )
  12:             return new string[] 
  13:             { string.Format( 
  14:                 "element {0}", 
  15:                 l.SelectedItems[0].Tag.GetType().Name ) };
  16:         else
  17:             return new string[] { string.Empty };
  18:     }
  20:     if ( Control is TreeView )
  21:     {
  22:         TreeView t = Control as TreeView;
  23:         if ( t.SelectedNode != null )
  24:             return BuildBindingContext( t.SelectedNode );
  25:         else
  26:             return new string[] { string.Empty };
  27:     }
  28:     #endregion
  30:     #region TabControl
  31:     if ( Control is TabControl )
  32:     {
  33:         TabControl tb = Control as TabControl;
  34:         if ( tb.SelectedTab != null )
  35:             return new string[] { tb.SelectedTab.Text };
  36:         else
  37:             return new string[] { string.Empty };
  38:     }
  39:     #endregion
  41:     return new string[] { Control.Text };
  42: }

So how it works?

Well, suppose the Topic is mapped for the TreeView and additional binding-contexts are defined. One named Context1 and mapped to Topic1 and Context2 mapped to Topic2.

Then, when F1 is pressed for any node except for Conext1 and Context2, the Topic is shown. However, when the node's text is Context1, Topic1 is shown and when the node's text is Context2, Topic2 is shown.

What's more interesting, the case of TreeView imposes that binding-contexts for controls are matched not only by the context name but, when there's no binding-context defined for selected node's text, the engine tries to find a context for selected node's parent, then for it's parent and so on.

This correctly handles a common scenario where a node name is fixed and set from within the code (e.g. Clients) but it's subnodes come from the database and thus their names are dynamic while the mapping expects a static, fixed set of names provided in a mapping file. In this scenario the same context will be assigned to Clients and all its subnodes (assuming that no specific binding context is assigned to subnodes).

There can be arbitrarily many of these additional binding-contexts for a control. 

As you can see in above code excerpt, the binding context for a ListView is determined by the item's Tag type but note also that this can be easily adopted to specific needs.

And how is the additional binding-context provided within the instrumentation mechanism?

Well, you just provide a topic mapping for the control as usual and then click "+" next to "Context" listbox.

The context's name is retrieved automatically from the application automatically and you just provide a topic mapping for it. You can also remove a context when it's not required anymore.


That's it. The new implementation just extends the former one which means that it should work correctly with help.mapping files generated with previous version of the library. When you compile the new source code, you would be able to extend the topic mapping with additional binding-contexts which will further refine the assignment of help topics to application's controls.

Feel free to use the code without any explicit permission. Leave a comment if you like this approach.


Hem Chandra Padhalni said...

Nice Article, However Can i use this whole concept of Context sensitive help in WPF applications, if yes what all changes i need to do?

Mike said...

Anybody worked out how to do the same when the focus is in gridview cells?