Integrated Help system in WPF Application

Posted on December 9, 2012. Filed under: .Net, C#, WPF | Tags: , , , |


Introduction

I have WPF application and I am thinking of creating help documentation for it. If anyone needs help on some screen and press F1, I don’t like to open traditional CHM or HTML based screens for that. So my plan is that the screen will describe its own help description and Controls will give their introduction and also show you the activity flow make user understand about basic flow the screen. But what if User also wants to introduction of each control click while working on a screen? Ok, then I will also print the basic introduction into the below status bar on mouse click of control (If it have any). It is just quick guideline to get rid of reading long and boring documentation and give you very basic information of a screen.

Still not clear to you?!! No problem. Let us take a simple screen to understand my thinking here:

A simple Login screen which controls:

  1. Textbox for User name
  2. Password box
  3. Login button
  4. And a button to open Wish-List Child Screen

Let us talk on flow first. The First Three controls have some flow to show. What I mean is that,

  1. User need to set username first
  2. Then enter the password
  3. And then click on login button

Concept

dhelp

So here you can see, I have also added a status bar to show the control description while selecting control (Here in picture login button has been selected). But if user asks for help for this screen by clicking F1, the screen should be look like this –

dhelp-hmode

To do this I have prepared documentation XML for this, Where I have kept all the necessary descriptions of a control like Title, Help Description, URL for see online Guide, Shot Cut Key/Hot Key and Flow Index (If Any) for ordering the activity flow sequences. You do need to synchronize the xml by yourself you have changes any workflow or short-cut/hot keys which is true for other documentation also. It off-course not a replacement of documentation guide, I just quick guideline for user. That is why I have kept an URL here to go to on-line guide for more details.

xml

Here I have put the element name as unique by I am going to map this documentation with the control use in UI. Flow Indexes has also been set. If any control which is not a part of some flow, I mean User can use it whenever he want e.g. search control, launching child window for settings or sending wish-list simply keep the flow Index empty.

No flow

And Result will be look something like this

Untitled

Using the code

To load and read this xml, I have prepared a Loader class which load and generate Dynamic Help data model based on chosen a language. I am maintaining multiple XMLs with same just like a Resource file do. In this sample, during the initialization of application I do xml loading and model generation in memory to cache. Later on I am going use these help definition based on some UI work.

   public class DynamicHelpStringLoader
    {
        private const string HelpStringReferenceFolder = "DynamicHelpReference";
        private const string UsFileName = "DynamicHelp_EN_US.xml";
        private const string FrFileName = "DynamicHelp_FR.xml";
        private const string EsFileName = "DynamicHelp_ES.xml";
        private const string DefaultFileName = "DynamicHelp_EN_US.xml";
 
        /// <summary>
        /// This is the collection where all the JerichoMessage objects
        /// will be stored.
        /// </summary>
        private static readonly Dictionary<string, DynamicHelpModel> HelpMessages;
 
        private static Languages _languageType;
 
        /// <summary>
        /// The static constructor.
        /// </summary>
        static DynamicHelpStringLoader()
        {
            HelpMessages = new Dictionary<string,DynamicHelpModel>();
            _languageType = Languages.None;
        }
        /// <summary>
        /// Generates the collection of JerichoMessage objects as if the provided language.
        /// </summary>
        /// <param name="languages">The Languages enum. Represents the user's choice of language.</param>
        public static void GenerateCollection(Languages languages)
        {
            if (_languageType == languages)
            {
                return;
            }
            _languageType = languages;
            string startUpPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly()
.GetModules()[0].FullyQualifiedName);
            string fileName;
            switch (languages)
            {
                case Languages.English:
                    fileName = UsFileName;
                    break;
                case Languages.French:
                    fileName = FrFileName;
                    break;
                case Languages.Spanish:
                    fileName = EsFileName;
                    break;
                default:
                    fileName = DefaultFileName;
                    break;
            }
 
            Task.Factory.StartNew(() =>
                                      {
                                          LoadXmlFile(Path.Combine(startUpPath,
                                                                   string.Format(@"{0}\{1}", HelpStringReferenceFolder,
                                                                                 fileName)));
                                      });
        }
        /// <summary>
        /// Load the provided xml file and populate the dictionary.
        /// </summary>
        /// <param name="fileName"></param>
        private static void LoadXmlFile(string fileName)
        {
            XDocument doc = null;
            try
            {
                //Load the XML Document                
                doc = XDocument.Load(fileName);
                //clear the dictionary
                HelpMessages.Clear();
 
                var helpCodeTypes = doc.Descendants("item");
                //now, populate the collection with JerichoMessage objects
                foreach (XElement message in helpCodeTypes)
                {
                    var key = message.Attribute("element_name").Value;
                    if(!string.IsNullOrWhiteSpace(key))
                    {
                        var index = 0;
                        //get all Message elements under the help type
                        //create a JerichoMessage object and insert appropriate values
                        var dynamicHelp = new DynamicHelpModel
                                              {
                                                  Title = message.Element("title").Value,
                                                  HelpText = message.Element("helptext").Value,
                                                  URL = message.Element("moreURL").Value,
                                                  ShortCut = message.Element("shortcut").Value,
                                                  FlowIndex = (int.TryParse(message.Element("flowindex").Value, out index)) ? index : 0
                                              };
                        //add the JerichoMessage into the collection
                        HelpMessages.Add(key.TrimStart().TrimEnd(), dynamicHelp);
                    }
                }
 
            }
            catch (FileNotFoundException)
            {
                throw new Exception(LanguageLoader.GetText("HelpCodeFileNotFound"));
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }
 
        /// <summary>
        /// Returns mathced string from the xml.
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public static DynamicHelpModel GetDynamicHelp(string name)
        {
            
            if(!string.IsNullOrWhiteSpace(name))
            {
                var key = name.TrimStart().TrimEnd();
                if(HelpMessages.ContainsKey(key))
                    return HelpMessages[key];
            }
            return new DynamicHelpModel();
        }
 

    } 

Now it is time to jump into UI work. I have created a attach property which enable dynamic help for a screen or window. So It will be simple Boolean attach property. On setting it, I am creating Help Group and add into a list. This list is necessary while working with child windows. The help group keeping is the element which has been enable as dynamic help. It is normally the root panel of a window .In sample, I have used the first child panel of window as element to dynamic help enable to get Adornerlayer where I can set the Text – “Help Model (Press F1 again to Exit)”.

You could look into Xaml here where I have put the element name and later on these unique string going to be mapped for retrieving help description.

xamluse

I also have kept the window and hooked closing event and mouse click and bind Command of ApplicationCommands.Help. Mouse Click event has been subscribed to get the find out the control it currently in and check for Help description to in status bar. Help command has been bound to get F1 pressed and toggle the help mode. In help mode I am going to find out all the controls with help description in the children of the element where you have setup the attached property. I need to hook the closing event here to clearing the Help Group with all its event-subscriptions.

        private static bool HelpActive { get; set; }
 
        public static void SetDynamicHelp(UIElement element, bool value)
        {
            element.SetValue(DynamicHelpProperty, value);
        }
        public static bool GetDynamicHelp(UIElement element)
        {
            return (Boolean)element.GetValue(DynamicHelpProperty);
        }
 
        public static readonly DependencyProperty DynamicHelpProperty =
          DependencyProperty.RegisterAttached("DynamicHelp", typeof(bool), typeof(UIElement),
                                              new PropertyMetadata(false, DynamicHelpChanged));
 
        private static readonly List<HelpGroup> HelpGroups = new List<HelpGroup>();
 
        public static HelpGroup Current
        {
            get
            {
                return HelpGroups.LastOrDefault();
            }
        }
 
        private static void DynamicHelpChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var element = d as UIElement;
 
            if (null != element)
            {
                if (null != HelpGroups && !HelpGroups.Any(g => null != g.Element && g.Element.Equals(element)))
                {
                    UIElement window = null;
                    if (element is Window)
                        window = (Window)element;
                    else
                        window = Window.GetWindow(element);
 
                    //Note: Use below code if you have used any custom window class other than 
                    //child of Window (for example WindowBase is base of your custom window)
                    //if (window == null)
                    //{
                    //    if (element is WindowBase)
                    //        window = (WindowBase)element;
                    //    else
                    //        window = element.TryFindParent<WindowBase>();
                    //}

                    if (null != window)
                    {
                        var currentGroup = new HelpGroup { Screen = window, Element = element, ScreenAdorner = 
new HelpTextAdorner(element) };
                        var newVal = (bool)e.NewValue;
                        var oldVal = (bool)e.OldValue;
                                              
                        // Register Events
                        if (newVal && !oldVal)
                        {
                            if (currentGroup.Screen != null)
                            {
                                if (!currentGroup.Screen.CommandBindings.OfType<CommandBinding>()
.Any(c => c.Command.Equals(ApplicationCommands.Help)))
                                {
                                    if (currentGroup._helpCommandBind == null)
                                    {
                                        currentGroup._helpCommandBind = 
new CommandBinding(ApplicationCommands.Help, HelpCommandExecute);
                                    }
                                    currentGroup.Screen.CommandBindings.Add(currentGroup._helpCommandBind);
                                }
 
                                if (currentGroup._helpHandler == null)
                                {
                                    currentGroup._helpHandler = new MouseButtonEventHandler(ElementMouse);
                                }
                                currentGroup.Screen.PreviewMouseLeftButtonDown += currentGroup._helpHandler;
                                if (window is Window)
                                    ((Window)currentGroup.Screen).Closing += WindowClosing;
                                //else
                                //((WindowBase)currentGroup.Screen).Closed += new EventHandler<WindowClosedEventArgs>(RadWindowClosed);
                            }
                        }
                        HelpGroups.Add(currentGroup);
                    }
                }
            }
 
        }
 

Lets come to mouse click event and how do I find the control with help description. Here it traverses to the top until I got the control with help description. On finding the control it will be able to show you the description in below status bar.

Here in this method ElementMouse a his test has been executed using InputHitTest to get control user clicked.  After that it check for help description, if not found it goes to parent and check. So I am traversing here to the top until I didn’t found nearer control with help description. 

static void ElementMouse(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            if(e.ButtonState != MouseButtonState.Pressed
                || e.ClickCount != 1)
                return;
 
            var element = sender as DependencyObject;
            if (null != element)
            {
                UIElement window = null;
                if (element is Window)
                    window = (Window)element;
                else
                    window = Window.GetWindow(element);
 
                //Note:  Use bellow code if you have used any custom window class other than child 
                //of Window (for example WindowBase is base of your custom window)
                //if (window == null)
                //{
                //    if (element is WindowBase)
                //        window = (WindowBase) element;
                //    else
                //        window = element.TryFindParent<WindowBase>();
                //}

                if (null != window)
                {
                    // Walk up the tree in case a parent element has help defined
                    var hitElement = (DependencyObject)window.InputHitTest(e.GetPosition(window));
 
                    var checkHelpDo = hitElement;                    
                    string helpText = Current.FetchHelpText(checkHelpDo);
                    while ( string.IsNullOrWhiteSpace(helpText) && checkHelpDo != null &&
                            !Equals(checkHelpDo, Current.Element) &&
                            !Equals(checkHelpDo, window))
                    {
                        checkHelpDo = (checkHelpDo is Visual)?  VisualTreeHelper.GetParent(checkHelpDo) : null;
                        helpText = Current.FetchHelpText(checkHelpDo);
                    }
                    if (string.IsNullOrWhiteSpace(helpText))
                    {
                        Current.HelpDO = null;
                    }
                    else if (!string.IsNullOrWhiteSpace(helpText) && Current.HelpDO != checkHelpDo)
                    {
                        Current.HelpDO = checkHelpDo;
                    }
 
                    if (null != OnHelpMessagePublished)
                       OnHelpMessagePublished(checkHelpDo, new HelperPublishEventArgs() { HelpMessage = helpText, Sender = hitElement});
                    
                }
            }
        }

On the help command execution it toggles the help mode. If help mode is true, then I have traversed the children recursively to find out all children with Help description and start a timer there to show popup on those controls in a sequential way.

        private static void DoGenerateHelpControl(DependencyObject dependObj, HelperModeEventArgs e)
        {            
            // Continue recursive toggle. Using the VisualTreeHelper works nicely.
            for (int x = 0; x < VisualTreeHelper.GetChildrenCount(dependObj); x++)
            {
                DependencyObject child = VisualTreeHelper.GetChild(dependObj, x);
                DoGenerateHelpControl(child, e);
            }
 
            // BitmapEffect is defined on UIElement so our DependencyObject 
            // must be a UIElement also
            if (dependObj is UIElement)
            {
                var element = (UIElement)dependObj;
                if (e.IsHelpActive)
                {
                    var helpText = e.Current.FetchHelpText(element);
                    if (!string.IsNullOrWhiteSpace(helpText) && element.IsVisible
                        && !IsWindowAdornerItem(element))
                    {
                        // Any effect can be used, I chose a simple yellow highlight
                        _helpElements.Add(new HelpElementArgs() { Element = element, 
                            HelpData = DynamicHelperViewer.GetPopUpTemplate(element, helpText, e.Current), 
                            Group = e.Current });
                    }
                }
                else if (element.Effect == HelpGlow)
                {
                    if(null != OnHelpTextCollaped)
                        OnHelpTextCollaped(null, new HelpElementArgs(){ Element =element, Group = e.Current});
                }
            }
        }   

Controls those don’t have any flow has been shown of first tick of timer. After that flow text has been shown with their flow index in sequential way. For this, I have found out the minimum flow index and then found out the data according to that index and show their pop up. I have also removed those from list since those has already been shown.

 public static void HelpTimerTick(object sender, ElapsedEventArgs args)
        {
            if(null != _helpElements && _helpElements.Count > 0)
            {
                int idx = _helpElements.Min(e => e.HelpData.Data.FlowIndex);
                var data = _helpElements.Where(e => e.HelpData.Data.FlowIndex.Equals(idx));
                foreach (var helpElementArgse in data.ToList())
                {
                    _helpElements.Remove(helpElementArgse);
                    if (null != OnHelpTextShown)
                    {
                        OnHelpTextShown(sender, helpElementArgse);
                    }   
                }                
            }
            else
            {
                _helpTimer.Enabled = false;
            }
        }  

For the child window, it going to give you same kind of result if you set enable to dynamic help for them.

dhelp-hmode CHILD

Points of Interest

To show the popup, I have used adorners as popup to get the relocate feature with controls on resize the screen and it will not be topmost of all application. But this pop up failed be on top of screen if I set my control near the boundaries of the screen. The result will occur cut off the popup since of unavailable space to show. I could use popup here with changing the topmost behaviour. I am trying to provide a concept here which doesn’t meet all the expectations or features we are expecting from documentation or help guide. But I think it give user quick guideline to understand a workflow of screen without reading a boring(!!) documentation guide.  So it is not the replacement of long documentation. You can keep both and allow user to open your long documentation from this quick documentation.

References

Read Full Post | Make a Comment ( None so far )

Liked it here?
Why not try sites on the blogroll...