Недавно мы рассказывали о том, как можно логировать действия пользователей в WinForms приложениях: Оно само упало, или следствие ведут колобки. Но что делать, если у вас WPF? Да нет проблем, и в WPF есть жизнь!
В WPF не надо будет вешать никаких хуков и трогать страшный винапи, собственно за пределы WPF мы и не выйдем. Для начала вспомним, что у нас есть routed events, и на них можно подписываться. В принципе, это все, что нам надо знать, чтобы реализовать поставленную задачу :)
Итак, что мы хотим логировать? Клавиатуру, мышку и смены фокуса. Для этого в классе UIElement есть следующие эвенты: PreviewMouseDownEvent, PreviewMouseUpEvent, PreviewKeyDownEvent, PreviewKeyUpEvent, PreviewTextInputEvent ну и Keyboard.GotKeyboardFocus и Keyboard.LostKeyboardFocus для фокуса. Теперь нам надо на них подписаться:
EventManager.RegisterClassHandler(
typeof(UIElement),
UIElement.PreviewMouseDownEvent,
new MouseButtonEventHandler(MouseDown),
true
);
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
);
Теперь главное, это написать обработчики всех этих эвентов, собрать на них данные о том, какую кнопку нажали, у кого, сколько раз… фу, скука. Вот, давайте лучше на котика посмотрим:
Ну а если вам очень хочется посмотреть код, то это можно сделать, раскрыв блок ниже
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;
}
Свойство Name появляется у нас во FrameworkElement, так что как source принимаем объект этого типа.
Теперь обработаем мышиные эвенты, в них мы соберем информацию о том, какую клавишу нажали и был ли это дабл клик или нет:
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);
}
В клавиатурных эвентах будем собирать Key. Однако, нам не хочется случайно утянуть вводимые пароли, поэтому хотелось бы понимать куда происходит ввод, чтобы заменять значение Key на Key.Multiply в случае ввода пароля. Узнать это мы можем при помощи AutomationPeer.IsPassword метода. И еще нюанс, не имеет смысла производить подобную замену при нажатии навигационных клавиш, ибо они точно не могут являться частью пароля, но могут быть отправной точкой для каких-либо иных действий. Например, смены фокуса по нажатию на Tab. В результате получаем следующее:
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;
}
Перейдем к TextInput. Тут, в принципе, все просто, собираем введенный текст и не забываем про пароли:
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);
}
Ну и, наконец, остался фокус:
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);
}
Обработчики готовы, пора тестить. Сделаем для этого простенькое приложение, добавим в него Logify и вперед:
Запустим его, введем q в текстовое поле и уроним приложение, нажав на Throw Exception и посмотрим, что же у нас собралось. Там получился страх и ужас, поэтому убрал под спойлер. Если точно хотите на это взглянуть, кликайте ниже:
Ээээ… Я думаю вы подумали как-то так:
Я именно так и подумал :)
Давайте разбираться, что у нас не так, и почему получилась такая портянка непонятных сообщений.
Первое, за что у меня цепляется взгляд, это куча эвентов о том, что фокус гуляет между двумя элементами. При этом объем этих сообщения равен чуть ли не половине общего объема логов. Дело в том, что фактически фокус был изменен один раз, но нотификацию об этом изменении мы получаем от каждого элемента по дереву, на которые мы подписаны. Ну а мы же не из анекдота, нам несколько раз повторять не надо. Поэтому давайте впишем проверочку:
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;
}
}
Посмотрим, что получилось:
Вот, гораздо красивее :)
Теперь мы видим, что у нас оооочень много логов на один и тот же эвент, так как routed эвенты идут по дереву элементов, и каждый из них оповещает нас. Дерево элементов у нас небольшое, а каши в логах уже предостаточно. Что же будет на реальном приложении? Даже боюсь подумать. Отбрасывать все эти логи, кроме первого или последнего, мы явно не можем. Если у вас достаточно большое визуальное дерево, то вряд ли вам что-то скажут сообщения о том, что кликнули в Window, или же в TextBox, особенно при отсутствии имен у элементов. Но в наших силах сократить этот список, чтобы его было удобно читать и при этом понимать, в каком именно месте произошло событие.
Мы подписались на эвенты у UIElement, но, по сути, сообщениями от большой части его наследников мы можем пренебречь. Например, вряд ли нам интересно уведомление о нажатии клавиши от Border или TextBlock. Эти элементы в большинстве своем не принимают участия в действиях. Как мне кажется, золотой серединой будет подписаться на эвенты у Control.
EventManager.RegisterClassHandler(
typeof(Control),
UIElement.PreviewMouseDownEvent,
new MouseButtonEventHandler(MouseDown),
true
);
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
);
В результате лог получился гораздо более читаемым, и, даже при бОльшем количестве эвентов, его смотреть не страшно:
Конечно, нет предела совершенству и у нас есть еще несколько трюков, как можно сделать этот лог еще более читаемым. Об этом будет одна из следующих наших статей.
Автор: byDesign