From 388df18e52efa177e694156607e369390e9de22e Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Fri, 7 Sep 2018 13:58:02 +0200 Subject: [PATCH 1/2] Initial commit of Event Viewer. --- .../Avalonia.Diagnostics.csproj | 8 ++ src/Avalonia.Diagnostics/DevTools.xaml | 1 + src/Avalonia.Diagnostics/DevTools.xaml.cs | 23 +++++ src/Avalonia.Diagnostics/Models/ChainLink.cs | 34 +++++++ .../ViewModels/ControlTreeNode.cs | 65 +++++++++++++ .../ViewModels/DevToolsViewModel.cs | 5 + .../ViewModels/EventEntryTreeNode.cs | 97 +++++++++++++++++++ .../ViewModels/EventTreeNode.cs | 80 +++++++++++++++ .../ViewModels/EventsViewModel.cs | 75 ++++++++++++++ .../ViewModels/FiredEvent.cs | 93 ++++++++++++++++++ .../Views/EventsView.xaml | 53 ++++++++++ .../Views/EventsView.xaml.cs | 28 ++++++ 12 files changed, 562 insertions(+) create mode 100644 src/Avalonia.Diagnostics/Models/ChainLink.cs create mode 100644 src/Avalonia.Diagnostics/ViewModels/ControlTreeNode.cs create mode 100644 src/Avalonia.Diagnostics/ViewModels/EventEntryTreeNode.cs create mode 100644 src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs create mode 100644 src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs create mode 100644 src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs create mode 100644 src/Avalonia.Diagnostics/Views/EventsView.xaml create mode 100644 src/Avalonia.Diagnostics/Views/EventsView.xaml.cs diff --git a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj index a2ff0ecf02..b807571a38 100644 --- a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj +++ b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj @@ -2,6 +2,9 @@ netstandard2.0 + + + @@ -17,4 +20,9 @@ + + + MSBuild:Compile + + \ No newline at end of file diff --git a/src/Avalonia.Diagnostics/DevTools.xaml b/src/Avalonia.Diagnostics/DevTools.xaml index 844670e794..a3b12f0ec5 100644 --- a/src/Avalonia.Diagnostics/DevTools.xaml +++ b/src/Avalonia.Diagnostics/DevTools.xaml @@ -3,6 +3,7 @@ + diff --git a/src/Avalonia.Diagnostics/DevTools.xaml.cs b/src/Avalonia.Diagnostics/DevTools.xaml.cs index be0863954a..d084ec3014 100644 --- a/src/Avalonia.Diagnostics/DevTools.xaml.cs +++ b/src/Avalonia.Diagnostics/DevTools.xaml.cs @@ -10,6 +10,7 @@ using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; +using Avalonia.Rendering; using Avalonia.VisualTree; namespace Avalonia @@ -28,6 +29,7 @@ namespace Avalonia.Diagnostics public class DevTools : UserControl { private static Dictionary s_open = new Dictionary(); + private static HashSet s_visualTreeRoots = new HashSet(); private IDisposable _keySubscription; public DevTools(IControl root) @@ -79,6 +81,7 @@ namespace Avalonia.Diagnostics devToolsWindow.Closed += devTools.DevToolsClosed; s_open.Add(control, devToolsWindow); + MarkAsDevTool(devToolsWindow); devToolsWindow.Show(); } } @@ -89,6 +92,7 @@ namespace Avalonia.Diagnostics var devToolsWindow = (Window)sender; var devTools = (DevTools)devToolsWindow.Content; s_open.Remove((TopLevel)devTools.Root); + RemoveDevTool(devToolsWindow); _keySubscription.Dispose(); devToolsWindow.Closed -= DevToolsClosed; } @@ -116,5 +120,24 @@ namespace Avalonia.Diagnostics } } } + + /// + /// Marks a visual as part of the DevTools, so it can be excluded from event tracking. + /// + /// The visual whose root is to be marked. + public static void MarkAsDevTool(IVisual visual) + { + s_visualTreeRoots.Add(visual.GetVisualRoot()); + } + + public static void RemoveDevTool(IVisual visual) + { + s_visualTreeRoots.Remove(visual.GetVisualRoot()); + } + + public static bool BelongsToDevTool(IVisual visual) + { + return s_visualTreeRoots.Contains(visual.GetVisualRoot()); + } } } diff --git a/src/Avalonia.Diagnostics/Models/ChainLink.cs b/src/Avalonia.Diagnostics/Models/ChainLink.cs new file mode 100644 index 0000000000..9ea99ff1ae --- /dev/null +++ b/src/Avalonia.Diagnostics/Models/ChainLink.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Interactivity; + +namespace Avalonia.Diagnostics.Models +{ + internal class ChainLink + { + public object Handler { get; private set; } + public string HandlerName + { + get + { + if (Handler is INamed named && !string.IsNullOrEmpty(named.Name)) + { + return named.Name + " (" + Handler.GetType().Name + ")"; + } + return Handler.GetType().Name; + } + } + public bool Handled { get; private set; } + public RoutingStrategies Route { get; private set; } + + public ChainLink(object handler, bool handled, RoutingStrategies route) + { + Contract.Requires(handler != null); + + this.Handler = handler; + this.Handled = handled; + this.Route = route; + } + } +} diff --git a/src/Avalonia.Diagnostics/ViewModels/ControlTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/ControlTreeNode.cs new file mode 100644 index 0000000000..d8a6a3bdc3 --- /dev/null +++ b/src/Avalonia.Diagnostics/ViewModels/ControlTreeNode.cs @@ -0,0 +1,65 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal class ControlTreeNode : EventTreeNode + { + public ControlTreeNode(Type type, IEnumerable events, EventsViewModel vm) + : base(null, type.Name) + { + this.Children = new AvaloniaList(events.OrderBy(e => e.Name).Select(e => new EventEntryTreeNode(this, e, vm) { IsEnabled = IsDefault(e) })); + this.IsExpanded = true; + } + + RoutedEvent[] defaultEvents = new RoutedEvent[] + { + Button.ClickEvent, + InputElement.KeyDownEvent, + InputElement.KeyUpEvent, + InputElement.TextInputEvent, + InputElement.PointerReleasedEvent, + InputElement.PointerPressedEvent, + }; + + private bool IsDefault(RoutedEvent e) + { + return defaultEvents.Contains(e); + } + + public override bool? IsEnabled + { + get => base.IsEnabled; + set + { + if (base.IsEnabled != value) + { + base.IsEnabled = value; + if (_updateChildren && value != null) + { + foreach (var child in Children) + { + try + { + child._updateParent = false; + child.IsEnabled = value; + } + finally + { + child._updateParent = true; + } + } + } + } + } + } + } +} diff --git a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs index ce8ad36c17..c6d3f02e8b 100644 --- a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs @@ -14,6 +14,7 @@ namespace Avalonia.Diagnostics.ViewModels private int _selectedTab; private TreePageViewModel _logicalTree; private TreePageViewModel _visualTree; + private EventsViewModel _eventsView; private string _focusedControl; private string _pointerOverElement; @@ -21,6 +22,7 @@ namespace Avalonia.Diagnostics.ViewModels { _logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root)); _visualTree = new TreePageViewModel(VisualTreeNode.Create(root)); + _eventsView = new EventsViewModel(root); UpdateFocusedControl(); KeyboardDevice.Instance.PropertyChanged += (s, e) => @@ -57,6 +59,9 @@ namespace Avalonia.Diagnostics.ViewModels case 1: Content = _visualTree; break; + case 2: + Content = _eventsView; + break; } RaisePropertyChanged(); diff --git a/src/Avalonia.Diagnostics/ViewModels/EventEntryTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/EventEntryTreeNode.cs new file mode 100644 index 0000000000..8eead869d5 --- /dev/null +++ b/src/Avalonia.Diagnostics/ViewModels/EventEntryTreeNode.cs @@ -0,0 +1,97 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Diagnostics.Models; +using Avalonia.Interactivity; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal class EventEntryTreeNode : EventTreeNode + { + RoutedEvent _event; + EventsViewModel _parentViewModel; + bool _isRegistered; + FiredEvent _currentEvent; + + public EventEntryTreeNode(ControlTreeNode parent, RoutedEvent @event, EventsViewModel vm) + : base(parent, @event.Name) + { + Contract.Requires(@event != null); + Contract.Requires(vm != null); + + this._event = @event; + this._parentViewModel = vm; + } + + public override bool? IsEnabled + { + get => base.IsEnabled; + set + { + if (base.IsEnabled != value) + { + base.IsEnabled = value; + UpdateTracker(); + if (Parent != null && _updateParent) + { + try + { + Parent._updateChildren = false; + Parent.UpdateChecked(); + } + finally + { + Parent._updateChildren = true; + } + } + } + } + } + + private void UpdateTracker() + { + if (IsEnabled.GetValueOrDefault() && !_isRegistered) + { + _event.AddClassHandler(typeof(object), HandleEvent, (RoutingStrategies)7, handledEventsToo: true); + _isRegistered = true; + } + } + + private void HandleEvent(object sender, RoutedEventArgs e) + { + if (!_isRegistered || IsEnabled == false) + return; + if (sender is IVisual v && DevTools.BelongsToDevTool(v)) + return; + + var s = sender; + var handled = e.Handled; + var route = e.Route; + + Action handler = delegate + { + if (_currentEvent == null || !_currentEvent.IsPartOfSameEventChain(e)) + { + _currentEvent = new FiredEvent(e, new ChainLink(s, handled, route)); + + _parentViewModel.RecordedEvents.Add(_currentEvent); + + while (_parentViewModel.RecordedEvents.Count > 100) + _parentViewModel.RecordedEvents.RemoveAt(0); + } + else + { + _currentEvent.AddToChain(new ChainLink(s, handled, route)); + } + }; + + if (!Dispatcher.UIThread.CheckAccess()) + Dispatcher.UIThread.Post(handler); + else + handler(); + } + } +} diff --git a/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs new file mode 100644 index 0000000000..50ea0c9c31 --- /dev/null +++ b/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs @@ -0,0 +1,80 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Diagnostics; +using Avalonia.Collections; +using Avalonia.VisualTree; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal abstract class EventTreeNode : ViewModelBase + { + internal bool _updateChildren = true; + internal bool _updateParent = true; + private bool _isExpanded; + private bool? _isEnabled = false; + + public EventTreeNode(EventTreeNode parent, string text) + { + this.Parent = parent; + this.Text = text; + } + + public IAvaloniaReadOnlyList Children + { + get; + protected set; + } + + public bool IsExpanded + { + get { return _isExpanded; } + set { RaiseAndSetIfChanged(ref _isExpanded, value); } + } + + public virtual bool? IsEnabled + { + get { return _isEnabled; } + set { RaiseAndSetIfChanged(ref _isEnabled, value); } + } + + public EventTreeNode Parent + { + get; + } + + public string Text + { + get; + private set; + } + + internal void UpdateChecked() + { + IsEnabled = GetValue(); + + bool? GetValue() + { + if (Children == null) + return false; + bool? value = false; + for (int i = 0; i < Children.Count; i++) + { + if (i == 0) + { + value = Children[i].IsEnabled; + continue; + } + + if (value != Children[i].IsEnabled) + { + value = null; + break; + } + } + + return value; + } + } + } +} diff --git a/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs new file mode 100644 index 0000000000..a9a6334689 --- /dev/null +++ b/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs @@ -0,0 +1,75 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Windows.Input; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Data.Converters; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Threading; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal class EventsViewModel : ViewModelBase + { + private IControl _root; + private FiredEvent _selectedEvent; + private ICommand ClearCommand { get; } + + public EventsViewModel(IControl root) + { + this._root = root; + this.Nodes = RoutedEventRegistry.Instance.GetAllRegistered() + .GroupBy(e => e.OwnerType) + .OrderBy(e => e.Key.Name) + .Select(g => new ControlTreeNode(g.Key, g, this)) + .ToArray(); + } + + private void ClearExecute() + { + Action action = delegate + { + RecordedEvents.Clear(); + }; + if (!Dispatcher.UIThread.CheckAccess()) + { + Dispatcher.UIThread.Post(action); + } + else + { + action(); + } + } + + public EventTreeNode[] Nodes { get; } + + public ObservableCollection RecordedEvents { get; } = new ObservableCollection(); + + public FiredEvent SelectedEvent + { + get => _selectedEvent; + set => RaiseAndSetIfChanged(ref _selectedEvent, value); + } + } + + internal class BoolToBrushConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return (bool)value ? Brushes.LightGreen : Brushes.Transparent; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs b/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs new file mode 100644 index 0000000000..523525b634 --- /dev/null +++ b/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs @@ -0,0 +1,93 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.ObjectModel; +using Avalonia.Diagnostics.Models; +using Avalonia.Interactivity; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal class FiredEvent : ViewModelBase + { + private RoutedEventArgs _eventArgs; + + private ChainLink _handledBy; + private ChainLink _originator; + + public FiredEvent(RoutedEventArgs eventArgs, ChainLink originator) + { + Contract.Requires(eventArgs != null); + Contract.Requires(originator != null); + + this._eventArgs = eventArgs; + this._originator = originator; + AddToChain(originator); + } + + public bool IsPartOfSameEventChain(RoutedEventArgs e) + { + return e == _eventArgs; + } + + public RoutedEvent Event => _eventArgs.RoutedEvent; + + public bool IsHandled => HandledBy?.Handled == true; + + public ObservableCollection EventChain { get; } = new ObservableCollection(); + + public string DisplayText + { + get + { + if (IsHandled) + { + return $"{Event.Name} on {Originator.HandlerName};" + Environment.NewLine + + $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}"; + } + return $"{Event.Name} on {Originator.HandlerName}; strategies: {Event.RoutingStrategies}"; + } + } + + public ChainLink Originator + { + get { return _originator; } + set + { + if (_originator != value) + { + _originator = value; + RaisePropertyChanged(); + RaisePropertyChanged(nameof(DisplayText)); + } + } + } + + public ChainLink HandledBy + { + get { return _handledBy; } + set + { + if (_handledBy != value) + { + _handledBy = value; + RaisePropertyChanged(); + RaisePropertyChanged(nameof(IsHandled)); + RaisePropertyChanged(nameof(DisplayText)); + } + } + } + + public void AddToChain(object handler, bool handled, RoutingStrategies route) + { + AddToChain(new ChainLink(handler, handled, route)); + } + + public void AddToChain(ChainLink link) + { + EventChain.Add(link); + if (HandledBy == null && link.Handled) + HandledBy = link; + } + } +} diff --git a/src/Avalonia.Diagnostics/Views/EventsView.xaml b/src/Avalonia.Diagnostics/Views/EventsView.xaml new file mode 100644 index 0000000000..79f78d2bba --- /dev/null +++ b/src/Avalonia.Diagnostics/Views/EventsView.xaml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +