Browse Source

Added EventRoute.

Instead of traversing the tree while raising an event, instead first build an event route and then raise the event along it.

Fixes #3176
pull/3621/head
Steven Kirk 6 years ago
parent
commit
cca4247c05
  1. 200
      src/Avalonia.Interactivity/EventRoute.cs
  2. 6
      src/Avalonia.Interactivity/EventSubscription.cs
  3. 7
      src/Avalonia.Interactivity/IInteractive.cs
  4. 195
      src/Avalonia.Interactivity/Interactive.cs
  5. 2
      src/Avalonia.Interactivity/RoutedEvent.cs
  6. 1
      tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs
  7. 2
      tests/Avalonia.UnitTests/MouseTestHelper.cs

200
src/Avalonia.Interactivity/EventRoute.cs

@ -0,0 +1,200 @@
using System;
using Avalonia.Collections.Pooled;
namespace Avalonia.Interactivity
{
/// <summary>
/// Holds the route for a routed event and supports raising an event on that route.
/// </summary>
public class EventRoute : IDisposable
{
private readonly RoutedEvent _event;
private PooledList<RouteItem>? _route;
/// <summary>
/// Initializes a new instance of the <see cref="RoutedEvent"/> class.
/// </summary>
/// <param name="e">The routed event to be raised.</param>
public EventRoute(RoutedEvent e)
{
e = e ?? throw new ArgumentNullException(nameof(e));
_event = e;
_route = null;
}
/// <summary>
/// Gets a value indicating whether the route has any handlers.
/// </summary>
public bool HasHandlers => _route?.Count > 0;
/// <summary>
/// Adds a handler to the route.
/// </summary>
/// <param name="target">The target on which the event should be raised.</param>
/// <param name="handler">The handler for the event.</param>
/// <param name="routes">The routing strategies to listen to.</param>
/// <param name="handledEventsToo">
/// If true the handler will be raised even when the routed event is marked as handled.
/// </param>
/// <param name="adapter">
/// An optional adapter which if supplied, will be called with <paramref name="handler"/>
/// and the parameters for the event. This adapter can be used to avoid calling
/// `DynamicInvoke` on the handler.
/// </param>
public void Add(
IInteractive target,
Delegate handler,
RoutingStrategies routes,
bool handledEventsToo = false,
Action<Delegate, object, RoutedEventArgs>? adapter = null)
{
target = target ?? throw new ArgumentNullException(nameof(target));
handler = handler ?? throw new ArgumentNullException(nameof(handler));
_route ??= new PooledList<RouteItem>(16);
_route.Add(new RouteItem(target, handler, adapter, routes, handledEventsToo));
}
/// <summary>
/// Adds a class handler to the route.
/// </summary>
/// <param name="target">The target on which the event should be raised.</param>
public void AddClassHandler(IInteractive target)
{
target = target ?? throw new ArgumentNullException(nameof(target));
_route ??= new PooledList<RouteItem>(16);
_route.Add(new RouteItem(target, null, null, 0, false));
}
/// <summary>
/// Raises an event along the route.
/// </summary>
/// <param name="source">The event source.</param>
/// <param name="e">The event args.</param>
public void RaiseEvent(IInteractive source, RoutedEventArgs e)
{
source = source ?? throw new ArgumentNullException(nameof(source));
e = e ?? throw new ArgumentNullException(nameof(e));
e.Source = source;
if (_event.RoutingStrategies == RoutingStrategies.Direct)
{
e.Route = RoutingStrategies.Direct;
RaiseEventImpl(e);
_event.InvokeRouteFinished(e);
}
else
{
if (_event.RoutingStrategies.HasFlagCustom(RoutingStrategies.Tunnel))
{
e.Route = RoutingStrategies.Tunnel;
RaiseEventImpl(e);
_event.InvokeRouteFinished(e);
}
if (_event.RoutingStrategies.HasFlagCustom(RoutingStrategies.Bubble))
{
e.Route = RoutingStrategies.Bubble;
RaiseEventImpl(e);
_event.InvokeRouteFinished(e);
}
}
}
/// <summary>
/// Disposes of the event route.
/// </summary>
public void Dispose()
{
_route?.Dispose();
_route = null;
}
private void RaiseEventImpl(RoutedEventArgs e)
{
if (_route is null)
{
return;
}
if (e.Source is null)
{
throw new ArgumentException("Event source may not be null", nameof(e));
}
IInteractive? lastTarget = null;
var start = 0;
var end = _route.Count;
var step = 1;
if (e.Route == RoutingStrategies.Tunnel)
{
start = end - 1;
step = end = -1;
}
for (var i = start; i != end; i += step)
{
var entry = _route[i];
// If we've got to a new control then call any RoutedEvent.Raised listeners.
if (entry.Target != lastTarget)
{
if (!e.Handled)
{
_event.InvokeRaised(entry.Target, e);
}
// If this is a direct event and we've already raised events then we're finished.
if (e.Route == RoutingStrategies.Direct && lastTarget is object)
{
return;
}
lastTarget = entry.Target;
}
// Raise the event handler.
if (entry.Handler is object &&
entry.Routes.HasFlagCustom(e.Route) &&
(!e.Handled || entry.HandledEventsToo))
{
if (entry.Adapter is object)
{
entry.Adapter(entry.Handler, entry.Target, e);
}
else
{
entry.Handler.DynamicInvoke(entry.Target, e);
}
}
}
}
private readonly struct RouteItem
{
public RouteItem(
IInteractive target,
Delegate? handler,
Action<Delegate, object, RoutedEventArgs>? adapter,
RoutingStrategies routes,
bool handledEventsToo)
{
Target = target;
Handler = handler;
Adapter = adapter;
Routes = routes;
HandledEventsToo = handledEventsToo;
}
public IInteractive Target { get; }
public Delegate? Handler { get; }
public Action<Delegate, object, RoutedEventArgs>? Adapter { get; }
public RoutingStrategies Routes { get; }
public bool HandledEventsToo { get; }
}
}
}

6
src/Avalonia.Interactivity/EventSubscription.cs

@ -5,15 +5,13 @@ using System;
namespace Avalonia.Interactivity
{
internal delegate void HandlerInvokeSignature(Delegate baseHandler, object sender, RoutedEventArgs args);
internal class EventSubscription
{
public EventSubscription(
Delegate handler,
RoutingStrategies routes,
bool handledEventsToo,
HandlerInvokeSignature? invokeAdapter = null)
Action<Delegate, object, RoutedEventArgs>? invokeAdapter = null)
{
Handler = handler;
Routes = routes;
@ -21,7 +19,7 @@ namespace Avalonia.Interactivity
InvokeAdapter = invokeAdapter;
}
public HandlerInvokeSignature? InvokeAdapter { get; }
public Action<Delegate, object, RoutedEventArgs>? InvokeAdapter { get; }
public Delegate Handler { get; }

7
src/Avalonia.Interactivity/IInteractive.cs

@ -60,6 +60,13 @@ namespace Avalonia.Interactivity
void RemoveHandler<TEventArgs>(RoutedEvent<TEventArgs> routedEvent, EventHandler<TEventArgs> handler)
where TEventArgs : RoutedEventArgs;
/// <summary>
/// Adds the object's handlers for a routed event to an event route.
/// </summary>
/// <param name="routedEvent">The event.</param>
/// <param name="route">The event route.</param>
void AddToEventRoute(RoutedEvent routedEvent, EventRoute route);
/// <summary>
/// Raises a routed event.
/// </summary>

195
src/Avalonia.Interactivity/Interactive.cs

@ -3,8 +3,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia.Layout;
using Avalonia.VisualTree;
@ -17,15 +15,14 @@ namespace Avalonia.Interactivity
{
private Dictionary<RoutedEvent, List<EventSubscription>>? _eventHandlers;
private static readonly Dictionary<Type, HandlerInvokeSignature> s_invokeHandlerCache = new Dictionary<Type, HandlerInvokeSignature>();
private static readonly Dictionary<Type, Action<Delegate, object, RoutedEventArgs>> s_invokeHandlerCache
= new Dictionary<Type, Action<Delegate, object, RoutedEventArgs>>();
/// <summary>
/// Gets the interactive parent of the object for bubbling and tunneling events.
/// </summary>
IInteractive? IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive;
private Dictionary<RoutedEvent, List<EventSubscription>> EventHandlers => _eventHandlers ??= new Dictionary<RoutedEvent, List<EventSubscription>>();
/// <summary>
/// Adds a handler for the specified routed event.
/// </summary>
@ -130,105 +127,83 @@ namespace Avalonia.Interactivity
throw new ArgumentException("Cannot raise an event whose RoutedEvent is null.");
}
e.Source ??= this;
if (e.RoutedEvent.RoutingStrategies == RoutingStrategies.Direct)
{
e.Route = RoutingStrategies.Direct;
RaiseEventImpl(e);
e.RoutedEvent.InvokeRouteFinished(e);
}
if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Tunnel) != 0)
{
TunnelEvent(e);
e.RoutedEvent.InvokeRouteFinished(e);
}
if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Bubble) != 0)
{
BubbleEvent(e);
e.RoutedEvent.InvokeRouteFinished(e);
}
using var route = BuildEventRoute(e.RoutedEvent);
route.RaiseEvent(this, e);
}
/// <summary>
/// Bubbles an event.
/// </summary>
/// <param name="e">The event args.</param>
private void BubbleEvent(RoutedEventArgs e)
void IInteractive.AddToEventRoute(RoutedEvent routedEvent, EventRoute route)
{
e = e ?? throw new ArgumentNullException(nameof(e));
e.Route = RoutingStrategies.Bubble;
var traverser = HierarchyTraverser<RaiseEventTraverse, NopTraverse>.Create(e);
traverser.Traverse(this);
}
/// <summary>
/// Tunnels an event.
/// </summary>
/// <param name="e">The event args.</param>
private void TunnelEvent(RoutedEventArgs e)
{
e = e ?? throw new ArgumentNullException(nameof(e));
e.Route = RoutingStrategies.Tunnel;
var traverser = HierarchyTraverser<NopTraverse, RaiseEventTraverse>.Create(e);
routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
route = route ?? throw new ArgumentNullException(nameof(route));
traverser.Traverse(this);
if (_eventHandlers != null &&
_eventHandlers.TryGetValue(routedEvent, out var subscriptions))
{
foreach (var sub in subscriptions)
{
route.Add(this, sub.Handler, sub.Routes, sub.HandledEventsToo, sub.InvokeAdapter);
}
}
}
/// <summary>
/// Carries out the actual invocation of an event on this object.
/// Builds an event route for a routed event.
/// </summary>
/// <param name="e">The event args.</param>
private void RaiseEventImpl(RoutedEventArgs e)
/// <param name="e">The routed event.</param>
/// <returns>An <see cref="EventRoute"/> describing the route.</returns>
/// <remarks>
/// Usually, calling <see cref="RaiseEvent(RoutedEventArgs)"/> is sufficent to raise a routed
/// event, however there are situations in which the construction of the event args is expensive
/// and should be avoided if there are no handlers for an event. In these cases you can call
/// this method to build the event route and check the <see cref="EventRoute.HasHandlers"/>
/// property to see if there are any handlers registered on the route. If there are, call
/// <see cref="EventRoute.RaiseEvent(IInteractive, RoutedEventArgs)"/> to raise the event.
/// </remarks>
protected EventRoute BuildEventRoute(RoutedEvent e)
{
e = e ?? throw new ArgumentNullException(nameof(e));
e.RoutedEvent!.InvokeRaised(this, e);
var result = new EventRoute(e);
var hasClassHandlers = e.HasRaisedSubscriptions;
if (_eventHandlers is object &&
_eventHandlers.TryGetValue(e.RoutedEvent, out var subscriptions) == true)
if (e.RoutingStrategies.HasFlagCustom(RoutingStrategies.Bubble) ||
e.RoutingStrategies.HasFlagCustom(RoutingStrategies.Tunnel))
{
foreach (var sub in subscriptions.ToList())
{
bool correctRoute = (e.Route & sub.Routes) != 0;
bool notFinished = !e.Handled || sub.HandledEventsToo;
IInteractive? element = this;
if (correctRoute && notFinished)
while (element != null)
{
if (hasClassHandlers)
{
if (sub.InvokeAdapter != null)
{
sub.InvokeAdapter(sub.Handler, this, e);
}
else
{
sub.Handler.DynamicInvoke(this, e);
}
result.AddClassHandler(element);
}
element.AddToEventRoute(e, result);
element = element.InteractiveParent;
}
}
}
private List<EventSubscription> GetEventSubscriptions(RoutedEvent routedEvent)
{
if (!EventHandlers.TryGetValue(routedEvent, out var subscriptions))
else
{
subscriptions = new List<EventSubscription>();
EventHandlers.Add(routedEvent, subscriptions);
if (hasClassHandlers)
{
result.AddClassHandler(this);
}
((IInteractive)this).AddToEventRoute(e, result);
}
return subscriptions;
return result;
}
private IDisposable AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription)
{
List<EventSubscription> subscriptions = GetEventSubscriptions(routedEvent);
_eventHandlers ??= new Dictionary<RoutedEvent, List<EventSubscription>>();
if (!_eventHandlers.TryGetValue(routedEvent, out var subscriptions))
{
subscriptions = new List<EventSubscription>();
_eventHandlers.Add(routedEvent, subscriptions);
}
subscriptions.Add(subscription);
@ -251,67 +226,5 @@ namespace Avalonia.Interactivity
_subscriptions.Remove(_subscription);
}
}
private interface ITraverse
{
void Execute(IInteractive target, RoutedEventArgs e);
}
private struct NopTraverse : ITraverse
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Execute(IInteractive target, RoutedEventArgs e)
{
}
}
private struct RaiseEventTraverse : ITraverse
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Execute(IInteractive target, RoutedEventArgs e)
{
((Interactive)target).RaiseEventImpl(e);
}
}
/// <summary>
/// Traverses interactive hierarchy allowing for raising events.
/// </summary>
/// <typeparam name="TPreTraverse">Called before parent is traversed.</typeparam>
/// <typeparam name="TPostTraverse">Called after parent has been traversed.</typeparam>
private struct HierarchyTraverser<TPreTraverse, TPostTraverse>
where TPreTraverse : struct, ITraverse
where TPostTraverse : struct, ITraverse
{
private TPreTraverse _preTraverse;
private TPostTraverse _postTraverse;
private readonly RoutedEventArgs _args;
private HierarchyTraverser(TPreTraverse preTraverse, TPostTraverse postTraverse, RoutedEventArgs args)
{
_preTraverse = preTraverse;
_postTraverse = postTraverse;
_args = args;
}
public static HierarchyTraverser<TPreTraverse, TPostTraverse> Create(RoutedEventArgs args)
{
return new HierarchyTraverser<TPreTraverse, TPostTraverse>(new TPreTraverse(), new TPostTraverse(), args);
}
public void Traverse(IInteractive target)
{
_preTraverse.Execute(target, _args);
var parent = target.InteractiveParent;
if (parent != null)
{
Traverse(parent);
}
_postTraverse.Execute(target, _args);
}
}
}
}

2
src/Avalonia.Interactivity/RoutedEvent.cs

@ -48,6 +48,8 @@ namespace Avalonia.Interactivity
public RoutingStrategies RoutingStrategies { get; }
public bool HasRaisedSubscriptions => _raised.HasObservers;
public IObservable<(object, RoutedEventArgs)> Raised => _raised;
public IObservable<RoutedEventArgs> RouteFinished => _routeFinished;

1
tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using Xunit;

2
tests/Avalonia.UnitTests/MouseTestHelper.cs

@ -56,7 +56,7 @@ namespace Avalonia.UnitTests
{
_pressedButton = mouseButton;
_pointer.Capture((IInputElement)target);
target.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, Timestamp(), props,
source.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, Timestamp(), props,
GetModifiers(modifiers), clickCount));
}
}

Loading…
Cancel
Save