Click here to Skip to main content
15,884,628 members
Articles / Desktop Programming / Windows Forms

Context Help Made Easy

Rate me:
Please Sign up or sign in to vote.
4.93/5 (53 votes)
2 Feb 2007CPOL10 min read 267K   2.7K   274  
This article introduces a new way of instrumenting your code that enables help authors associate help topics with the application’s visual contexts at any time - even post-compilation – and to do so using the application’s user interface without the involvement of the developer.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Windows.Forms;
using System.Text;
using System.Xml;

namespace ContextHelpMadeEasy
{
    /// <summary>
    /// Utility class for maintaining relationship between context Id and path.
    /// </summary>
    public class ContextIDHTMLPathMap
    {
        public string ContextID;
        public string HTMLPath;

        public ContextIDHTMLPathMap(string ID, string Path)
        {
            ContextID = ID;
            HTMLPath = Path;
        }
    }
    /// <summary>
    /// This class implements support for context sensitive help, including both runtime 
    /// behavior of looking up the appropriate help file and launching help, and the 
    /// development time behavior of permitting help writers to create mappings between
    /// a user context and an HTML help page.
    /// </summary>
    public static class HelpUtility
    {
        /// <summary>
        /// The local cache of ContextID to HTMLHelp Paths mapping.
        /// </summary>
        static private StringDictionary ms_sdContextPaths = null;
        private const string mc_sMAPPING_FILE_NAME = "HelpContextMapping.Config";
        private const string mc_sIDMAP_ELEMENT_NAME = "IDMap";
        private const string mc_sCONTEXTID_ELEMENT_NAME = "ContextID";
        private const string mc_sID_ATTRIBUTE_NAME = "ID";
        private const string mc_sHTMLPATH_ATTRIBUTE_NAME = "HTMLPath";
        private const string mc_sHELPFILE = "ContextHelpMadeEasy.chm";

        #region Public Methods

        /// <summary>
        /// Process an F1 or Ctrl-F1 request.
        /// </summary>
        /// <param name="ctrContext"></param>
        public static void ProcessHelpRequest(Control ctrContext)
        {
            // You will probably want to add an additional test for some registry key
            // here.  Set this key if you want to enable the Help Mapping feature.
            if (Control.ModifierKeys == Keys.Control)
            {
                ShowHelpMappingDialog(ctrContext);
                return;
            }
            ShowContextHelp(ctrContext);
        }
        /// <summary>
        /// Process a request to display help for the context specified by ctrContext.
        /// Go up the parent chain until we find a control where:
        ///   1. The control implements IContextHelp,
        ///   2. The control has a non-null IContextHelp.ContextHelpID, and
        ///   3. The ContextHelpID has a corresponding entry in the mapping XML file.
        /// 
        /// If this is found, launch the help viewer to display it.
        /// If it is not found, launch the help viewer to display the default topic for
        /// the Application.
        /// </summary>
        /// <param name="ctrContext"></param>
        public static void ShowContextHelp(Control ctrContext)
        {
            Control ctr = ctrContext;

            string sHTMLFileName = null;
            while (ctr != null)
            {
                // Get the first control in the parent chain that supports the IContextHelp interface.
                IContextHelp help = GetIContextHelpControl(ctr);

                // If there isn't one, display the default help for the application.
                if (help == null)
                    break;

                // Check to see if it has a ContextHelpID value.
                if (help.ContextHelpID != null)
                {
                    // Check to see if the ID has a mapped HTML file name.
                    sHTMLFileName = LookupHTMLHelpPathFromID(help.ContextHelpID);
                    if (sHTMLFileName != null && ShowHelp(ctrContext, sHTMLFileName))
                        return;
                }
                // Get the parent control and repeat.
                ctr = ((Control)help).Parent;
            }
            // Show the default topic.
            ShowHelp(ctrContext, "");
        }

        /// <summary>
        /// Process a request to pop up a mapping dialog that permits the help writer to 
        /// associate an HTML path with the current context.
        /// 
        /// Traverse the parent control chain looking for controls that implement the
        /// IContextHelp interface.  For each one found, add it to the list of available
        /// contexts.  Include the associated HTML path if it's defined.
        /// 
        /// Finally, show the dialog for the help writer to edit the mappings.
        /// </summary>
        /// <param name="ctrContext"></param>
        public static void ShowHelpMappingDialog(Control ctrContext)
        {
            IContextHelp help = GetIContextHelpControl(ctrContext);

            List<ContextIDHTMLPathMap> alContextPaths = new List<ContextIDHTMLPathMap>();

            // Create a list of contexts starting with the current help context
            // and moving up the parent chain.
            while (help != null)
            {
                string sContextID = help.ContextHelpID;
                if (sContextID != null)
                {
                    string sHTMLHelpPath = LookupHTMLHelpPathFromID(sContextID);
                    alContextPaths.Add(new ContextIDHTMLPathMap(sContextID, sHTMLHelpPath));
                }
                help = GetIContextHelpControl(((Control)help).Parent);
            }

            // Pop up the mapping dialog. If it returns true, this means a change was made
            // so we rewrite the XML mapping file with the new information.
            if (FHelpMappingDialog.ShowHelpWriterHelper(alContextPaths) == true)
            {
                foreach (ContextIDHTMLPathMap pathMap in alContextPaths)
                {
                    if (!string.IsNullOrEmpty(pathMap.ContextID))
                    {
                        if (!string.IsNullOrEmpty(pathMap.HTMLPath))
                        {
                            ContextPaths[pathMap.ContextID] = pathMap.HTMLPath;
                        }
                        else
                        {
                            if (ContextPaths.ContainsKey(pathMap.ContextID))
                                ContextPaths.Remove(pathMap.ContextID);
                        }
                    }
                }
                SaveMappingFile(ContextPaths);
            }
        }

        #endregion // Public Methods

        #region Private Properties
        /// <summary>
        /// Return the cached ContextPaths string dictionary. Creates it
        /// from the Mapping XML file if it doesn't exist.
        /// </summary>
        private static StringDictionary ContextPaths
        {
            get
            {
                if (ms_sdContextPaths == null)
                {
                    ms_sdContextPaths = ReadMappingFile();
                }
                return ms_sdContextPaths;
            }
        }
        /// <summary>
        /// Return the path to the mapping file.
        /// </summary>
        private static string MappingFilePath { get { return Path.Combine(System.Windows.Forms.Application.StartupPath, mc_sMAPPING_FILE_NAME); } }

        /// <summary>
        /// Return the path to the CHM file.
        /// </summary>
        private static string HelpFilePath { get { return Path.Combine(System.Windows.Forms.Application.StartupPath, mc_sHELPFILE); } }

        #endregion // Private Properties

        #region Private Methods

        /// <summary>
        /// Given an ID, return the associated HTML Help path
        /// </summary>
        /// <param name="sContextID"></param>
        /// <returns></returns>
        private static string LookupHTMLHelpPathFromID(string sContextID)
        {
            if (ContextPaths.ContainsKey(sContextID))
                return ContextPaths[sContextID];
            return null;
        }

        /// <summary>
        /// Display the specified help page.
        /// </summary>
        /// <param name="sHTMLHelp"></param>
        private static bool ShowHelp(Control ctlContext, string sHTMLHelp)
        {
            try
            {
                if (string.IsNullOrEmpty(sHTMLHelp))
                    Help.ShowHelp(ctlContext, HelpUtility.HelpFilePath);
                else
                    Help.ShowHelp(ctlContext, HelpUtility.HelpFilePath, HelpNavigator.Topic, sHTMLHelp);
            }
            catch (ArgumentException)
            {
                // Ideally, we would return false when the HTML file isn't found in the CHM file.
                // Unfortunately, there doesn't seem to be a way to do this.  
                return false;
            }
            return true;
        }

        /// <summary>
        /// Read the mapping file to create a list of ID to HTML file mappings.
        /// This method returns a StringDictionary containing the list.
        /// </summary>
        /// <returns></returns>
        private static StringDictionary ReadMappingFile()
        {
            StringDictionary sdMapping = new StringDictionary();
            XmlDocument docMapping = new XmlDocument();

            if (File.Exists(MappingFilePath) == true)
            {
                try
                {
                    docMapping.Load(MappingFilePath);
                }
                catch
                {
                    MessageBox.Show(string.Format("Could not read help mapping file '{0}'", MappingFilePath), "Context Help Made Easy", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    throw;
                }

                XmlNodeList nlMappings = docMapping.SelectNodes("//" + mc_sCONTEXTID_ELEMENT_NAME);
                foreach (XmlElement el in nlMappings)
                {
                    string sID = el.GetAttribute(mc_sID_ATTRIBUTE_NAME);
                    string sPath = el.GetAttribute(mc_sHTMLPATH_ATTRIBUTE_NAME);
                    if (sID != "" && sPath != "")
                        sdMapping.Add(sID, sPath);
                }
            }
            return sdMapping;
        }

        /// <summary>
        /// Saves the specified StringDictionary that contains ID to Path mappings to the
        /// XML mapping file.
        /// </summary>
        /// <param name="sdMappings"></param>
        private static void SaveMappingFile(StringDictionary sdMappings)
        {
            // Create a new XML document and initialize it with the XML declaration and the
            // outer IDMap element.
            XmlDocument docMapping = new XmlDocument();
            XmlDeclaration xmlDecl = docMapping.CreateXmlDeclaration("1.0", null, null);
            docMapping.InsertBefore(xmlDecl, docMapping.DocumentElement);
            XmlElement elIDMap = AddChildElementToNode(docMapping, docMapping, mc_sIDMAP_ELEMENT_NAME);
            // Add the defined mappings between contextID and filename.
            foreach (DictionaryEntry de in sdMappings)
            {
                XmlElement elMapping = AddChildElementToNode(elIDMap, docMapping, mc_sCONTEXTID_ELEMENT_NAME);
                elMapping.SetAttribute(mc_sID_ATTRIBUTE_NAME, de.Key as string);
                elMapping.SetAttribute(mc_sHTMLPATH_ATTRIBUTE_NAME, de.Value as string);
            }
            try
            {
                docMapping.Save(MappingFilePath);
            }
            catch
            {
                MessageBox.Show(string.Format("Could not write help mapping file '{0}'", MappingFilePath), "Context Help Made Easy", MessageBoxButtons.OK, MessageBoxIcon.Error);
                throw;
            }
        }

        /// <summary>
        /// Small utility method to add XML elements to a parent node.
        /// </summary>
        /// <param name="node"></param>
        /// <param name="elementName"></param>
        /// <returns></returns>
        private static XmlElement AddChildElementToNode(XmlNode node, XmlDocument doc, string elementName)
        {
            XmlElement el = doc.CreateElement(elementName);
            node.AppendChild(el);
            return el;
        }

        /// <summary>
        /// Get the first control in the parent chain (including the control passed in)
        /// that implements IContextHelp.
        /// </summary>
        /// <param name="ctl"></param>
        /// <param name="ctlIContextHelp"></param>
        private static IContextHelp GetIContextHelpControl(Control ctl)
        {
            while (ctl != null)
            {
                IContextHelp help = ctl as IContextHelp;
                if (help != null)
                {
                    return help;
                }
                ctl = ctl.Parent;
            }
            return null;
        }
    }
        #endregion // Private methods

}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Product Manager
United States United States
I've been programming in C, C++, Visual Basic and C# for over 35 years. I've worked at Sierra Systems, ViewStar, Mosaix, Lucent, Avaya, Avinon, Apptero, Serena and now Guidewire Software in various roles over my career.

Comments and Discussions