We collect user activity in WPF
Recently, we talked about how you can log user actions in WinForms applications: It itself fell, or koloboks lead the investigation . But what if you have WPF? No problem, and WPF has life!
In WPF, you don’t have to hang any hooks or touch a terrible winpy, actually we won’t go beyond WPF. To begin with, we recall that we have routed events , and you can subscribe to them. In principle, this is all we need to know in order to realize the task :)
So, what do we want to log? Keyboard, mouse and focus changes. For this, the following events are in the UIElement class: PreviewMouseDownEvent , PreviewMouseUpEvent , PreviewKeyDownEvent ,
PreviewKeyUpEvent , PreviewTextInputEvent and Keyboard.GotKeyboardFocus and Keyboard.LostKeyboardFocus for focus. Now we need to subscribe to them:
Now the main thing is to write the handlers of all these events, collect data on them about which button was pressed, from whom, how many times ... phew, boredom. Now, let's look at the cat better:
Well, if you really want to see the code, then this can be done by opening the block below
Handlers are ready, it's time to test. Let's make a simple application for this, add Logify to it and go:
Run it, enter q in the text box and drop the application by clicking on Throw Exception and see what we have gathered. There turned out fear and horror, so I removed it under the spoiler. If you really want to take a look at this, click below:
Eeee ... I think you thought something like this:
I thought just that :)
Let's figure out what is wrong with us and why we got such a mess of obscure messages.
The first thing that my gaze clings to is a bunch of events that focus is walking between two elements. Moreover, the volume of these messages is almost half of the total volume of logs. The fact is that in fact the focus was 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. So let's enter a check:
Let's see what happened:
Now, it’s much more beautiful :)
Now we see that we have sooooo many logs for the same event, since routed events go through the tree of elements, and each of them notifies us. We have a small tree of elements, and there are already plenty of cereals in the logs. What will happen on a real application? I'm even afraid to think. We cannot explicitly discard all these logs except the first or last. If you have a large enough visual tree, it is unlikely that you will be told something by a message that you clicked on a Window, or in a TextBox, especially if there are no names for the elements. But it is in our power to shorten this list so that it is convenient to read and at the same time understand in which place the event took place.
We subscribed to events at UIElement, but, in fact, we can neglect messages from most of his heirs. For example, we are hardly interested in the notification of a keystroke from Border or TextBlock. Most of these elements do not take part in actions. It seems to me that the middle ground will be to subscribe to Control events.
As a result, the log turned out to be much more readable, and even with a large number of events, it is not scary to watch it:
Of course, there is no limit to perfection and we have a few more tricks on how to make this log even more readable. This will be one of our next articles.
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 the handlers of all these events, collect data on them about which button was pressed, from whom, how many times ... phew, boredom. Now, let's look at the cat better:
Well, if you really want to see the code, then 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 a method that collects information common to all events: the name and type of the element that sent this event:
The Name property appears in our FrameworkElement, so as source we accept an object of this type.
Now we will process the mouse events, in them we will collect information about which key was pressed and whether it was a double click or not:
In keyboard events we will collect Key. However, we don’t want to accidentally drag out the entered passwords, so we would like to understand where the input is going to replace the Key value with Key.Multiply in case of entering the password. We can find out using the AutomationPeer.IsPassword method. And another nuance, it does not make sense to make a similar replacement when pressing the navigation keys, because they certainly can not be part of the password, but can be the starting point for any other actions. For example, changing focus by pressing Tab. As a result, we get the following:
Let's move on to TextInput. Here, in principle, everything is simple, we collect the entered text and do not forget about passwords:
Well, finally, the focus remained:
Dictionary CollectCommonProperties(FrameworkElement source) {
Dictionary properties = new Dictionary();
properties["Name"] = source.Name;
properties["ClassName"] = source.GetType().ToString();
return properties;
}
The Name property appears in our FrameworkElement, so as source we accept an object of this type.
Now we will process the 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 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 drag out the entered passwords, so we would like to understand where the input is going to replace the Key value with Key.Multiply in case of entering the password. We can find out using the AutomationPeer.IsPassword method. And another nuance, it does not make sense to make a similar replacement when pressing the navigation keys, because they certainly can not be part of the password, but can be the starting point for any other actions. For example, changing focus by pressing Tab. As a result, we get 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 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 move on 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 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);
}
Well, 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 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:
Run it, enter q in the text box and drop the application by clicking on Throw Exception and see what we have gathered. There turned out fear and horror, so I removed it under the spoiler. If you really want to take a look at this, click below:
Very big log
Eeee ... I think you thought something like this:
I thought just that :)
Let's figure out what is wrong with us and why we got such a mess of obscure messages.
The first thing that my gaze clings to is a bunch of events that focus is walking between two elements. Moreover, the volume of these messages is almost half of the total volume of logs. The fact is that in fact the focus was 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. So let's enter a check:
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:
Now, it’s much more beautiful :)
Now we see that we have sooooo many logs for the same event, since routed events go through the tree of elements, and each of them notifies us. We have a small tree of elements, and there are already plenty of cereals in the logs. What will happen on a real application? I'm even afraid to think. We cannot explicitly discard all these logs except the first or last. If you have a large enough visual tree, it is unlikely that you will be told something by a message that you clicked on a Window, or in a TextBox, especially if there are no names for the elements. But it is in our power to shorten this list so that it is convenient to read and at the same time understand in which place the event took place.
We subscribed to events at UIElement, but, in fact, we can neglect messages from most of his heirs. For example, we are hardly interested in the notification of a keystroke from Border or TextBlock. Most of these elements do not take part in actions. It seems to me that the middle ground will be to subscribe to Control events.
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 scary to watch it:
Of course, there is no limit to perfection and we have a few more tricks on how to make this log even more readable. This will be one of our next articles.