Question

How are you instrumenting your UI's? In the past I've read that people have instrumented their user interfaces, but what I haven't found is examples or tips on how to instrument a UI.

By instrumenting, I mean collecting data regarding usage and performance of the system. A MSDN article on Instrumentation is http://msdn.microsoft.com/en-us/library/x5952w0c.aspx. I would like to capture which buttons users click on, what keyboard shortucts they use, what terms they use to search, etc.

  • How are you instrumenting your UI?
  • What format are you storing the instrumentation?
  • How are you processing the instrumented data?
  • How are you keeping your UI code clean with this instrumentation logic?

Specifically, I am implementing my UI in WPF, so this will provide extra challenges compared to instrumenting a web-based application. (i.e. need to transfer the instrumented data back to a central location, etc). That said, I feel the technology may provide an easier implementation of instrumentation via concepts like attached properties.

  • Have you instrumented a WPF application? Do you have any tips on how this can be achieved?

Edit: The following blog post presents an interesting solution: Pixel-In-Gene Blog: Techniques for UI Auditing on WPF apps

Was it helpful?

Solution 4

The following blog post gives quite a few good ideas for instrumenting a WPF application: Techniques for UI Auditing on WPF apps.

OTHER TIPS

Here is an example of how I use a simple events manager to hook on to the UI events and extract key information of the events, such as name and type of UI element, name of event and the parent window's type name. For lists I also extract the selected item.

This solution only listens for clicks of controls derived from ButtonBase (Button, ToggleButton, ...) and selection changes in controls derived from Selector (ListBox, TabControl, ...). It should be easy to extend to other types of UI elements or to achieve a more fine-grained solution. The solution is inspired of Brad Leach's answer.

public class UserInteractionEventsManager
{
    public delegate void ButtonClickedHandler(DateTime time, string eventName, string senderName, string senderTypeName, string parentWindowName);
    public delegate void SelectorSelectedHandler(DateTime time, string eventName, string senderName, string senderTypeName, string parentWindowName, object selectedObject);

    public event ButtonClickedHandler ButtonClicked;
    public event SelectorSelectedHandler SelectorSelected;

    public UserInteractionEventsManager()
    {
        EventManager.RegisterClassHandler(typeof(ButtonBase), ButtonBase.ClickEvent, new RoutedEventHandler(HandleButtonClicked));
        EventManager.RegisterClassHandler(typeof(Selector), Selector.SelectionChangedEvent, new RoutedEventHandler(HandleSelectorSelected));
    }

    #region Handling events

    private void HandleSelectorSelected(object sender, RoutedEventArgs e)
    {
        // Avoid multiple events due to bubbling. Example: A ListBox inside a TabControl will cause both to send the SelectionChangedEvent.
        if (sender != e.OriginalSource) return;

        var args = e as SelectionChangedEventArgs;
        if (args == null || args.AddedItems.Count == 0) return;

        var element = sender as FrameworkElement;
        if (element == null) return;

        string senderName = GetSenderName(element);
        string parentWindowName = GetParentWindowTypeName(sender);
        DateTime time = DateTime.Now;
        string eventName = e.RoutedEvent.Name;
        string senderTypeName = sender.GetType().Name;
        string selectedItemText = args.AddedItems.Count > 0 ? args.AddedItems[0].ToString() : "<no selected items>";

        if (SelectorSelected != null)
            SelectorSelected(time, eventName, senderName, senderTypeName, parentWindowName, selectedItemText);
    }

    private void HandleButtonClicked(object sender, RoutedEventArgs e)
    {
        var element = sender as FrameworkElement;
        if (element == null) return;

        string parentWindowName = GetParentWindowTypeName(sender);
        DateTime time = DateTime.Now;
        string eventName = e.RoutedEvent.Name;
        string senderTypeName = sender.GetType().Name;
        string senderName = GetSenderName(element);

        if (ButtonClicked != null) 
            ButtonClicked(time, eventName, senderName, senderTypeName, parentWindowName);
    }

    #endregion

    #region Private helpers

    private static string GetSenderName(FrameworkElement element)
    {
        return !String.IsNullOrEmpty(element.Name) ? element.Name : "<no item name>";
    }


    private static string GetParentWindowTypeName(object sender)
    {
        var parent = FindParent<Window>(sender as DependencyObject);
        return parent != null ? parent.GetType().Name : "<no parent>";
    }

    private static T FindParent<T>(DependencyObject item) where T : class
    {
        if (item == null) 
            return default(T);

        if (item is T)
            return item as T;

        DependencyObject parent = VisualTreeHelper.GetParent(item);
        if (parent == null)
            return default(T);

        return FindParent<T>(parent);
    }

    #endregion
}

And to do the actual logging, I use log4net and created a separate logger named 'Interaction' to log user interaction. The class 'Log' here is simply my own static wrapper for log4net.

/// <summary>
/// The user interaction logger uses <see cref="UserInteractionEventsManager"/> to listen for events on GUI elements, such as buttons, list boxes, tab controls etc.
/// The events are then logged in a readable format using Log.Interaction.Info().
/// </summary>
public class UserInteractionLogger
{
    private readonly UserInteractionEventsManager _events;
    private bool _started;

    /// <summary>
    /// Create a user interaction logger. Remember to Start() it.
    /// </summary>
    public UserInteractionLogger()
    {
        _events = new UserInteractionEventsManager();

    }

    /// <summary>
    /// Start logging user interaction events.
    /// </summary>
    public void Start()
    {
        if (_started) return;

        _events.ButtonClicked += ButtonClicked;
        _events.SelectorSelected += SelectorSelected;

        _started = true;
    }

    /// <summary>
    /// Stop logging user interaction events.
    /// </summary>
    public void Stop()
    {
        if (!_started) return;

        _events.ButtonClicked -= ButtonClicked;
        _events.SelectorSelected -= SelectorSelected;

        _started = false;
    }

    private static void SelectorSelected(DateTime time, string eventName, string senderName, string senderTypeName, string parentWindowTypeName, object selectedObject)
    {
        Log.Interaction.Info("{0}.{1} by {2} in {3}. Selected: {4}", senderTypeName, eventName, senderName, parentWindowTypeName, selectedObject);
    }

    private static void ButtonClicked(DateTime time, string eventName, string senderName, string senderTypeName, string parentWindowTypeName)
    {
        Log.Interaction.Info("{0}.{1} by {2} in {3}", senderTypeName, eventName, senderName, parentWindowTypeName);
    }
}

The output would then look something like this, omitting non-relevant log entries.

04/13 08:38:37.069 INFO        Iact ToggleButton.Click by AnalysisButton in MyMainWindow
04/13 08:38:38.493 INFO        Iact ListBox.SelectionChanged by ListView in MyMainWindow. Selected: Andreas Larsen
04/13 08:38:44.587 INFO        Iact Button.Click by EditEntryButton in MyMainWindow
04/13 08:38:46.068 INFO        Iact Button.Click by OkButton in EditEntryDialog
04/13 08:38:47.395 INFO        Iact ToggleButton.Click by ExitButton in MyMainWindow

You could consider log4net. It is a robust logging framework that exists in a single DLL. It is also done in a "non demanding" type mode so that if a critical process is going on, it won't log until resources are freed up a bit more.

You could easily setup a bunch of INFO level loggers and track all the user interaction you needed, and it wouldn't take a bug crash to send the file to yourself. You could also then log all your ERROR and FATAL code to seperate file that could easily be mailed to you for processing.

If you make use of WPF commands, each custom command could then log the Action taken. You can also log the way the command was initiated.

Perhaps the Microsoft UI Automation for WPF can help out ? Its a framework for automating your UI, perhaps it can be used to log stuff for you...

We use the Automation Framework for auto-testing our UI in WPF.

Disclaimer: I work for the company that sells this product, not only that but I am a developer on this particular product :) .

If you are interested in a commercial product to provide this then Runtime Intelligence (a functional add on to Dotfuscator ) that injects usage tracking functionality into your .NET applications is available. We provide not only the actual implementation of the tracking functionality but the data collection, processing and reporting functionality as well.

There was recently a discussion on the Business of Software forum on this topic that I also posted in located here: http://discuss.joelonsoftware.com/default.asp?biz.5.680205.26 .

For a high level overview of our stuff see here: http://www.preemptive.com/runtime-intelligence-services.html .

In addition I am currently working on writing up some more technically oriented documentation as we realize that is an area we could definitely improve, please let me know if anyone is interested in being notified when I have completed it.

I have not yet developed using WPF.. But I would assume that its the same as most other applications in that you want to keep the UI code as light as possible.. A number of design patterns may be used in this such as the obvious MVC and Façade. I personally always try and keep the objects travelling between the UI and BL layers as light as possible, keeping them to primitives if I can.

This then helps me focus on improving the UI layer without the concerns of anything going on once I throw my (primitive) data back..

I hope I understood your question correctly, and sorry I cannot offer more contextual help with WPF.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top