📜 ⬆️ ⬇️

We collected the user activity in WPF

Recently, we talked about how you can log user actions in WinForms applications: It fell by itself, or koloboks lead the investigation . But what if you have WPF? No problem, and WPF has life!



In WPF, you will not need to hang any hooks and touch a terrible Winapi; we will not go outside WPF itself. First, remember that we have routed events , and you can subscribe to them. In principle, this is all we need to know in order to accomplish the task :)

So what do we want to log? Keyboard, mouse and focus changes. For this, the UIElement class has the following events: PreviewMouseDownEvent , PreviewMouseUpEvent , PreviewKeyDownEvent , PreviewKeyUpEvent , PreviewTextInputEvent and Keyboard.GotKeyboardFocus and Keyboard.LostKeyboardFocus for focus. Now we need to subscribe to them:
')
EventManager.RegisterClassHandler( typeof(UIElement), UIElement.PreviewMouseDownEvent, new MouseButtonEventHandler(MouseDown), true ); 

Subscribe to other events
 EventManager.RegisterClassHandler( typeof(UIElement), UIElement.PreviewMouseUpEvent, new MouseButtonEventHandler(MouseUp), true ); EventManager.RegisterClassHandler( typeof(UIElement), UIElement.PreviewKeyDownEvent, new KeyEventHandler(KeyDown), true ); EventManager.RegisterClassHandler( typeof(UIElement), UIElement.PreviewKeyUpEvent, new KeyEventHandler(KeyUp), true ); EventManager.RegisterClassHandler( typeof(UIElement), UIElement.PreviewTextInputEvent, new TextCompositionEventHandler(TextInput), true ); EventManager.RegisterClassHandler( typeof(UIElement), Keyboard.GotKeyboardFocusEvent, new KeyboardFocusChangedEventHandler(OnKeyboardFocusChanged), true ); EventManager.RegisterClassHandler( typeof(UIElement), Keyboard.LostKeyboardFocusEvent, new KeyboardFocusChangedEventHandler(OnKeyboardFocusChanged), true ); 

Now the main thing is to write handlers of all these events, to collect data on them about which button was pressed, from whom, how many times ... fu, boredom. Here, let's take a better look at the cat:

image

Well, if you really want to see the code, this can be done by opening the block below.

a lot of code
Let's start writing handlers for these events. Let's start with the method that collects common information for all events: the name and type of the element that sent this event:

 Dictionary<string, string> CollectCommonProperties(FrameworkElement source) { Dictionary<string, string> properties = new Dictionary<string, string>(); properties["Name"] = source.Name; properties["ClassName"] = source.GetType().ToString(); return properties; } 

The Name property appears in our FrameworkElement, so as a source, we accept an object of this type.

Now we will process mouse events, in them we will collect information about which key was pressed and whether it was a double click or not:

 void MouseDown(object sender, MouseButtonEventArgs e) { FrameworkElement source = sender as FrameworkElement; if(source == null) return; var properties = CollectCommonProperties(source); LogMouse(properties, e, isUp: false); } void MouseUp(object sender, MouseButtonEventArgs e) { FrameworkElement source = sender as FrameworkElement; if(source == null) return; var properties = CollectCommonProperties(source); LogMouse(properties, e, isUp: true); } void LogMouse(IDictionary<string, string> properties, MouseButtonEventArgs e, bool isUp) { properties["mouseButton"] = e.ChangedButton.ToString(); properties["ClickCount"] = e.ClickCount.ToString(); Breadcrumb item = new Breadcrumb(); if(e.ClickCount == 2) { properties["action"] = "doubleClick"; item.Event = BreadcrumbEvent.MouseDoubleClick; } else if(isUp) { properties["action"] = "up"; item.Event = BreadcrumbEvent.MouseUp; } else { properties["action"] = "down"; item.Event = BreadcrumbEvent.MouseDown; } item.CustomData = properties; AddBreadcrumb(item); } 

In keyboard events we will collect Key. However, we don’t want to accidentally reduce the entered passwords, so I would like to understand where the input is going to replace the Key value with Key.Multiply in the case of a password input. We can learn this using the AutomationPeer.IsPassword method. And another nuance, it does not make sense to make such a replacement when you press the navigation keys, because they definitely can not be part of the password, but can be the starting point for any other actions. For example, changing focus by pressing Tab. The result is the following:

 void KeyDown(object sender, KeyEventArgs e) { FrameworkElement source = sender as FrameworkElement; if(source == null) return; var properties = CollectCommonProperties(source); LogKeyboard(properties, e.Key, isUp: false, isPassword: CheckPasswordElement(e.OriginalSource as UIElement)); } void KeyUp(object sender, KeyEventArgs e) { FrameworkElement source = sender as FrameworkElement; if(source == null) return; var properties = CollectCommonProperties(source); LogKeyboard(properties, e.Key, isUp: true, isPassword: CheckPasswordElement(e.OriginalSource as UIElement)); } void LogKeyboard(IDictionary<string, string> properties, Key key, bool isUp, bool isPassword) { properties["key"] = GetKeyValue(key, isPassword).ToString(); properties["action"] = isUp ? "up" : "down"; Breadcrumb item = new Breadcrumb(); item.Event = isUp ? BreadcrumbEvent.KeyUp : BreadcrumbEvent.KeyDown; item.CustomData = properties; AddBreadcrumb(item); } Key GetKeyValue(Key key, bool isPassword) { if(!isPassword) return key; switch(key) { case Key.Tab: case Key.Left: case Key.Right: case Key.Up: case Key.Down: case Key.PageUp: case Key.PageDown: case Key.LeftCtrl: case Key.RightCtrl: case Key.LeftShift: case Key.RightShift: case Key.Enter: case Key.Home: case Key.End: return key; default: return Key.Multiply; } } bool CheckPasswordElement(UIElement targetElement) { if(targetElement != null) { AutomationPeer automationPeer = GetAutomationPeer(targetElement); return (automationPeer != null) ? automationPeer.IsPassword() : false; } return false; } 

Let's go to TextInput. Here, in principle, everything is simple, we collect the entered text and do not forget about passwords:

 void TextInput(object sender, TextCompositionEventArgs e) { FrameworkElement source = sender as FrameworkElement; if(source == null) return; var properties = CollectCommonProperties(source); LogTextInput(properties, e, CheckPasswordElement(e.OriginalSource as UIElement)); } void LogTextInput(IDictionary<string, string> properties, TextCompositionEventArgs e, bool isPassword) { properties["text"] = isPassword ? "*" : e.Text; properties["action"] = "press"; Breadcrumb item = new Breadcrumb(); item.Event = BreadcrumbEvent.KeyPress; item.CustomData = properties; AddBreadcrumb(item); } 

And finally, the focus remained:

 void OnKeyboardFocusChanged(object sender, KeyboardFocusChangedEventArgs e) { FrameworkElement oldFocus = e.OldFocus as FrameworkElement; if(oldFocus != null) { var properties = CollectCommonProperties(oldFocus); LogFocus(properties, isGotFocus: false); } FrameworkElement newFocus = e.NewFocus as FrameworkElement; if(newFocus != null) { var properties = CollectCommonProperties(newFocus); LogFocus(properties, isGotFocus: true); } } void LogFocus(IDictionary<string, string> properties, bool isGotFocus) { Breadcrumb item = new Breadcrumb(); item.Event = isGotFocus ? BreadcrumbEvent.GotFocus : BreadcrumbEvent.LostFocus; item.CustomData = properties; AddBreadcrumb(item); } 

Handlers are ready, it's time to test. Let's make a simple application for this, add Logify to it and go ahead:



Run it, enter q in the text field and drop the application by clicking on Throw Exception and see what we have gathered. It turned out fear and horror, so put it under the spoiler. If you just want to look at it, click below:

Very large log

Ehhh ... I think you thought something like this:

image

I thought so :)

Let's figure out what's wrong with us, and why such a messy of incomprehensible messages has turned out.

The first thing my eyes cling to is a bunch of events that the focus is walking between two elements. At the same time, the volume of these messages is almost half of the total volume of logs. The fact is that the focus was actually changed once, but we receive a notification about this change from each element in the tree to which we are subscribed. Well, we are not from a joke, we do not need to repeat several times. Therefore, let's enter the test:

 IInputElement FocusedElement { get; set; } void OnKeyboardFocusChanged(object sender, KeyboardFocusChangedEventArgs e) { if(FocusedElement != e.NewFocus) { FrameworkElement oldFocus = FocusedElement as FrameworkElement; if(oldFocus != null) { var properties = CollectCommonProperties(oldFocus); LogFocus(properties, false); } FrameworkElement newFocus = e.NewFocus as FrameworkElement; if(newFocus != null) { var properties = CollectCommonProperties(newFocus); LogFocus(properties, true); } FocusedElement = e.NewFocus; } } 

Let's see what happened:



Here, much more beautiful :)

Now we see that we have soooo many logs for the same event, as the routed events go through the tree of elements, and each of them notifies us. We have a small tree of elements, and there is already plenty of porridge in the logs. What will be on the real application? Even afraid to think. We obviously cannot discard all these logs, except the first or the last. If you have a large enough visual tree, then it’s unlikely that you will be told something about the messages that were clicked in the Window, or in the TextBox, especially if there are no names for the elements. But we are able to reduce this list so that it is easy to read and at the same time understand exactly where the event occurred.

We have subscribed to events at UIElement, but, in fact, we can ignore messages from a large part of his heirs. For example, we are hardly interested in a keystroke notification from a Border or TextBlock. These elements for the most part do not take part in actions. It seems to me that the golden mean will be to sign up for events at Control.

 EventManager.RegisterClassHandler( typeof(Control), UIElement.PreviewMouseDownEvent, new MouseButtonEventHandler(MouseDown), true ); 

Other events
 EventManager.RegisterClassHandler( typeof(Control), UIElement.PreviewMouseUpEvent, new MouseButtonEventHandler(MouseUp), true ); EventManager.RegisterClassHandler( typeof(Control), UIElement.PreviewKeyDownEvent, new KeyEventHandler(KeyDown), true ); EventManager.RegisterClassHandler( typeof(Control), UIElement.PreviewKeyUpEvent, new KeyEventHandler(KeyUp), true ); EventManager.RegisterClassHandler( typeof(Control), UIElement.PreviewTextInputEvent, new TextCompositionEventHandler(TextInput), true ); EventManager.RegisterClassHandler( typeof(Control), Keyboard.GotKeyboardFocusEvent, new KeyboardFocusChangedEventHandler(OnKeyboardFocusChanged), true ); EventManager.RegisterClassHandler( typeof(Control), Keyboard.LostKeyboardFocusEvent, new KeyboardFocusChangedEventHandler(OnKeyboardFocusChanged), true ); 

As a result, the log turned out to be much more readable, and, even with a large number of events, it is not terrible to watch it:



Of course, there is no limit to perfection, and we still have a few tricks on how to make this log even more readable. This will be one of our next articles.

Source: https://habr.com/ru/post/343358/


All Articles