This is a quick guideline to get rid of reading long and boring documentation and give you very basic information about a screen.
Introduction
I have a WPF application and I am thinking of creating a help documentation for it. If anyone needs help on some screen and presses F1, I don’t like to open a traditional CHM or HTML based screen 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 to make the user understand the basic flow of the screen. But what if the user also wants an introduction to each control click while working on a screen? OK, then I will also print the basic introduction into the below status bar on the mouse click of the control (if it has any). It is just a quick guideline to get rid of reading long and boring documentation and give you a 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:
- Textbox for user name
- Password box
- Login button
- And a button to open a Wish-List child screen
Let us talk about the flow first. The first three controls have some flow to show. What I mean is that:
- User needs to set the username first
- Then enter the password
- And then click on the login button
Concept
So here you can see that I have also added a status bar to show the control description while selecting a control (here in the picture, the login button has been selected). But if the user asks for help for this screen by clicking F1, the screen should look like this:
To do this, I have prepared XML documentation, where I have kept all the necessary descriptions of a control like Title, Help Description, URL for an online guide, shortcut key/hot key, and flow index (if any) for ordering the activity flow sequences. You do need to synchronize the XML yourself if you have changes to any workflow or short-cut/hot keys, which is true for other documentation also. It, of course, is not a replacement for a documentation guide, just a quick guideline for the user. That is why I have kept a URL here to go to the on-line guide for more details.
Here, I have put the element name as unique as I am going to map this documentation with the control used in the UI. Flow Indexes have also been set. If a control is not a part of some flow, I mean the user can use it whenever he wants, e.g., search control, launching the child window for settings or sending a wish-list simply keeps the flow index empty.
And the result will look something like this:
Using the Code
To load and read this XML, I have prepared a loader class which loads and generates a Dynamic Help data model based on the chosen language. I am maintaining multiple XMLs same just like a resource file does. In this sample, during the initialization of the application, I do XML loading and model generation in memory to cache. Later on, I am going use these help definitions 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";
private static readonly Dictionary<string, DynamicHelpModel> HelpMessages;
private static Languages _languageType;
static DynamicHelpStringLoader()
{
HelpMessages = new Dictionary<string,DynamicHelpModel>();
_languageType = Languages.None;
}
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)));
});
}
private static void LoadXmlFile(string fileName)
{
XDocument doc = null;
try
{
doc = XDocument.Load(fileName);
HelpMessages.Clear();
var helpCodeTypes = doc.Descendants("item");
foreach (XElement message in helpCodeTypes)
{
var key = message.Attribute("element_name").Value;
if(!string.IsNullOrWhiteSpace(key))
{
var index = 0;
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
};
HelpMessages.Add(key.TrimStart().TrimEnd(), dynamicHelp);
}
}
}
catch (FileNotFoundException)
{
throw new Exception(LanguageLoader.GetText("HelpCodeFileNotFound"));
}
catch (Exception ex)
{
throw ex;
}
}
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 the UI work. I have created an attach property which enables dynamic help for a screen or window. So it will be a simple boolean attach property. On setting it, I am creating a Help Group and adding into a list. This list is necessary while working with child windows. The help group keeping is the element which has been enabled as dynamic help. It is normally the root panel of a window. In the sample, I have used the first child panel of the window as the element to enable dynamic help to get the Adorner
layer where I can set the Text – “Help Model (Press F1 again to Exit)”.
You could look into the XAML here where I have put the element name and later on, these unique string
s are mapped for retrieving help description.
I also have kept the window and hooked closing event and mouse click and bound Command of ApplicationCommands.Help
. The mouse click event has been subscribed to find out the control it currently is in and checks for Help description in status bar. The 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 the help description in the children of the element where you have setup the attached property. I need to hook the closing event here to clear 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);
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;
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;
}
}
HelpGroups.Add(currentGroup);
}
}
}
}
Let's come to the mouse click event and how I find the control with the help description. Here, it traverses to the top until I got the control with the help description. On finding the control, it will be able to show you the description in the below status bar.
Here in this method, ElementMouse
, a hit test has been executed using InputHitTest
to get the control the user clicked. After that, it checks for the help description, if not found, it goes to the parent and checks. So I am traversing here to the top until I find the nearest 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);
if (null != window)
{
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 the help mode is true, I have traversed the children recursively to find out all the children with Help description and started a timer there to show a popup on those controls in a sequential way.
private static void DoGenerateHelpControl(DependencyObject dependObj, HelperModeEventArgs e)
{
for (int x = 0; x < VisualTreeHelper.GetChildrenCount(dependObj); x++)
{
DependencyObject child = VisualTreeHelper.GetChild(dependObj, x);
DoGenerateHelpControl(child, e);
}
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))
{
_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 that don’t have any flow have been shown on the first tick of the timer. After that, the flow text has been shown with the flow index in a sequential way. For this, I have found out the minimum flow index and then found out the data according to that index and shown their pop up. I have also removed those from the list since those have 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 will give you the same kind of result if you enable dynamic help for them.
Points of Interest
To show the popup, I have tried to resolve some popup issue like updating location of popup while dragging the window and changing the WindowState
. Also, a behaviour has been added to resolve the issue popup relocation of Window resizing. I wasn't able to test all cases here, so it may don't work in all cases. I am hoping you give me feedback if you have faced any unusual things. But I am trying to provide a concept here which doesn't meet all the expectations or features we are expecting from the documentation or help guide. But I think it gives users a quick guideline to understand the workflow of a screen without reading a boring(!!) documentation guide. So it is not a replacement for long documentation. You can keep both and allow the user to open your long documentation from this quick documentation.
References
History
- 20th June, 2013: Initial version