Browse Source

Merge pull request #3607 from AvaloniaUI/fixes/3176-event-route

Build event routes before raising events.
pull/3652/head
Steven Kirk 7 years ago
committed by GitHub
parent
commit
9f551ac410
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      src/Avalonia.Interactivity/Avalonia.Interactivity.csproj
  2. 200
      src/Avalonia.Interactivity/EventRoute.cs
  3. 20
      src/Avalonia.Interactivity/EventSubscription.cs
  4. 9
      src/Avalonia.Interactivity/IInteractive.cs
  5. 283
      src/Avalonia.Interactivity/Interactive.cs
  6. 5
      src/Avalonia.Interactivity/InteractiveExtensions.cs
  7. 20
      src/Avalonia.Interactivity/RoutedEvent.cs
  8. 8
      src/Avalonia.Interactivity/RoutedEventArgs.cs
  9. 6
      src/Avalonia.Interactivity/RoutedEventRegistry.cs
  10. 25
      tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs
  11. 2
      tests/Avalonia.UnitTests/MouseTestHelper.cs

6
src/Avalonia.Interactivity/Avalonia.Interactivity.csproj

@ -1,6 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<Nullable>Enable</Nullable>
<WarningsAsErrors>CS8600;CS8602;CS8603</WarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" /> <ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />
@ -9,6 +11,4 @@
<ProjectReference Include="..\Avalonia.Visuals\Avalonia.Visuals.csproj" /> <ProjectReference Include="..\Avalonia.Visuals\Avalonia.Visuals.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\Rx.props" /> <Import Project="..\..\build\Rx.props" />
</Project> </Project>

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; }
}
}
}

20
src/Avalonia.Interactivity/EventSubscription.cs

@ -1,20 +0,0 @@
// 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;
namespace Avalonia.Interactivity
{
internal delegate void HandlerInvokeSignature(Delegate baseHandler, object sender, RoutedEventArgs args);
internal class EventSubscription
{
public HandlerInvokeSignature InvokeAdapter { get; set; }
public Delegate Handler { get; set; }
public RoutingStrategies Routes { get; set; }
public bool AlsoIfHandled { get; set; }
}
}

9
src/Avalonia.Interactivity/IInteractive.cs

@ -13,7 +13,7 @@ namespace Avalonia.Interactivity
/// <summary> /// <summary>
/// Gets the interactive parent of the object for bubbling and tunneling events. /// Gets the interactive parent of the object for bubbling and tunneling events.
/// </summary> /// </summary>
IInteractive InteractiveParent { get; } IInteractive? InteractiveParent { get; }
/// <summary> /// <summary>
/// Adds a handler for the specified routed event. /// Adds a handler for the specified routed event.
@ -60,6 +60,13 @@ namespace Avalonia.Interactivity
void RemoveHandler<TEventArgs>(RoutedEvent<TEventArgs> routedEvent, EventHandler<TEventArgs> handler) void RemoveHandler<TEventArgs>(RoutedEvent<TEventArgs> routedEvent, EventHandler<TEventArgs> handler)
where TEventArgs : RoutedEventArgs; 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> /// <summary>
/// Raises a routed event. /// Raises a routed event.
/// </summary> /// </summary>

283
src/Avalonia.Interactivity/Interactive.cs

@ -3,8 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -15,16 +13,12 @@ namespace Avalonia.Interactivity
/// </summary> /// </summary>
public class Interactive : Layoutable, IInteractive public class Interactive : Layoutable, IInteractive
{ {
private Dictionary<RoutedEvent, List<EventSubscription>> _eventHandlers; private Dictionary<RoutedEvent, List<EventSubscription>>? _eventHandlers;
private static readonly Dictionary<Type, HandlerInvokeSignature> s_invokeHandlerCache = new Dictionary<Type, HandlerInvokeSignature>();
/// <summary> /// <summary>
/// Gets the interactive parent of the object for bubbling and tunneling events. /// Gets the interactive parent of the object for bubbling and tunneling events.
/// </summary> /// </summary>
IInteractive IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive; IInteractive? IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive;
private Dictionary<RoutedEvent, List<EventSubscription>> EventHandlers => _eventHandlers ?? (_eventHandlers = new Dictionary<RoutedEvent, List<EventSubscription>>());
/// <summary> /// <summary>
/// Adds a handler for the specified routed event. /// Adds a handler for the specified routed event.
@ -40,16 +34,10 @@ namespace Avalonia.Interactivity
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
bool handledEventsToo = false) bool handledEventsToo = false)
{ {
Contract.Requires<ArgumentNullException>(routedEvent != null); routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
Contract.Requires<ArgumentNullException>(handler != null); handler = handler ?? throw new ArgumentNullException(nameof(handler));
var subscription = new EventSubscription
{
Handler = handler,
Routes = routes,
AlsoIfHandled = handledEventsToo,
};
var subscription = new EventSubscription(handler, routes, handledEventsToo);
return AddEventSubscription(routedEvent, subscription); return AddEventSubscription(routedEvent, subscription);
} }
@ -68,35 +56,18 @@ namespace Avalonia.Interactivity
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
bool handledEventsToo = false) where TEventArgs : RoutedEventArgs bool handledEventsToo = false) where TEventArgs : RoutedEventArgs
{ {
Contract.Requires<ArgumentNullException>(routedEvent != null); routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
Contract.Requires<ArgumentNullException>(handler != null); handler = handler ?? throw new ArgumentNullException(nameof(handler));
// EventHandler delegate is not covariant, this forces us to create small wrapper
// that will cast our type erased instance and invoke it.
Type eventArgsType = routedEvent.EventArgsType;
if (!s_invokeHandlerCache.TryGetValue(eventArgsType, out var invokeAdapter)) static void InvokeAdapter(Delegate baseHandler, object sender, RoutedEventArgs args)
{ {
void InvokeAdapter(Delegate baseHandler, object sender, RoutedEventArgs args) var typedHandler = (EventHandler<TEventArgs>)baseHandler;
{ var typedArgs = (TEventArgs)args;
var typedHandler = (EventHandler<TEventArgs>)baseHandler;
var typedArgs = (TEventArgs)args;
typedHandler(sender, typedArgs); typedHandler(sender, typedArgs);
}
invokeAdapter = InvokeAdapter;
s_invokeHandlerCache.Add(eventArgsType, invokeAdapter);
} }
var subscription = new EventSubscription var subscription = new EventSubscription(handler, routes, handledEventsToo, (baseHandler, sender, args) => InvokeAdapter(baseHandler, sender, args));
{
InvokeAdapter = invokeAdapter,
Handler = handler,
Routes = routes,
AlsoIfHandled = handledEventsToo,
};
return AddEventSubscription(routedEvent, subscription); return AddEventSubscription(routedEvent, subscription);
} }
@ -108,14 +79,19 @@ namespace Avalonia.Interactivity
/// <param name="handler">The handler.</param> /// <param name="handler">The handler.</param>
public void RemoveHandler(RoutedEvent routedEvent, Delegate handler) public void RemoveHandler(RoutedEvent routedEvent, Delegate handler)
{ {
Contract.Requires<ArgumentNullException>(routedEvent != null); routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
Contract.Requires<ArgumentNullException>(handler != null); handler = handler ?? throw new ArgumentNullException(nameof(handler));
List<EventSubscription> subscriptions = null;
if (_eventHandlers?.TryGetValue(routedEvent, out subscriptions) == true) if (_eventHandlers is object &&
_eventHandlers.TryGetValue(routedEvent, out var subscriptions))
{ {
subscriptions.RemoveAll(x => x.Handler == handler); for (var i = subscriptions.Count - 1; i >= 0; i--)
{
if (subscriptions[i].Handler == handler)
{
subscriptions.RemoveAt(i);
}
}
} }
} }
@ -137,112 +113,117 @@ namespace Avalonia.Interactivity
/// <param name="e">The event args.</param> /// <param name="e">The event args.</param>
public void RaiseEvent(RoutedEventArgs e) public void RaiseEvent(RoutedEventArgs e)
{ {
Contract.Requires<ArgumentNullException>(e != null); e = e ?? throw new ArgumentNullException(nameof(e));
e.Source = 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) if (e.RoutedEvent == null)
{ {
TunnelEvent(e); throw new ArgumentException("Cannot raise an event whose RoutedEvent is null.");
e.RoutedEvent.InvokeRouteFinished(e);
} }
if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Bubble) != 0) using var route = BuildEventRoute(e.RoutedEvent);
{ route.RaiseEvent(this, e);
BubbleEvent(e);
e.RoutedEvent.InvokeRouteFinished(e);
}
} }
/// <summary> void IInteractive.AddToEventRoute(RoutedEvent routedEvent, EventRoute route)
/// Bubbles an event.
/// </summary>
/// <param name="e">The event args.</param>
private void BubbleEvent(RoutedEventArgs e)
{ {
Contract.Requires<ArgumentNullException>(e != null); routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
route = route ?? throw new ArgumentNullException(nameof(route));
e.Route = RoutingStrategies.Bubble; if (_eventHandlers != null &&
_eventHandlers.TryGetValue(routedEvent, out var subscriptions))
var traverser = HierarchyTraverser<RaiseEventTraverse, NopTraverse>.Create(e); {
foreach (var sub in subscriptions)
traverser.Traverse(this); {
} route.Add(this, sub.Handler, sub.Routes, sub.HandledEventsToo, sub.InvokeAdapter);
}
/// <summary> }
/// Tunnels an event.
/// </summary>
/// <param name="e">The event args.</param>
private void TunnelEvent(RoutedEventArgs e)
{
Contract.Requires<ArgumentNullException>(e != null);
e.Route = RoutingStrategies.Tunnel;
var traverser = HierarchyTraverser<NopTraverse, RaiseEventTraverse>.Create(e);
traverser.Traverse(this);
} }
/// <summary> /// <summary>
/// Carries out the actual invocation of an event on this object. /// Builds an event route for a routed event.
/// </summary> /// </summary>
/// <param name="e">The event args.</param> /// <param name="e">The routed event.</param>
private void RaiseEventImpl(RoutedEventArgs e) /// <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)
{ {
Contract.Requires<ArgumentNullException>(e != null); e = e ?? throw new ArgumentNullException(nameof(e));
e.RoutedEvent.InvokeRaised(this, e);
List<EventSubscription> subscriptions = null; var result = new EventRoute(e);
var hasClassHandlers = e.HasRaisedSubscriptions;
if (_eventHandlers?.TryGetValue(e.RoutedEvent, out subscriptions) == true) if (e.RoutingStrategies.HasFlagCustom(RoutingStrategies.Bubble) ||
e.RoutingStrategies.HasFlagCustom(RoutingStrategies.Tunnel))
{ {
foreach (var sub in subscriptions.ToList()) IInteractive? element = this;
{
bool correctRoute = (e.Route & sub.Routes) != 0;
bool notFinished = !e.Handled || sub.AlsoIfHandled;
if (correctRoute && notFinished) while (element != null)
{
if (hasClassHandlers)
{ {
if (sub.InvokeAdapter != null) result.AddClassHandler(element);
{
sub.InvokeAdapter(sub.Handler, this, e);
}
else
{
sub.Handler.DynamicInvoke(this, e);
}
} }
element.AddToEventRoute(e, result);
element = element.InteractiveParent;
} }
} }
else
{
if (hasClassHandlers)
{
result.AddClassHandler(this);
}
((IInteractive)this).AddToEventRoute(e, result);
}
return result;
} }
private List<EventSubscription> GetEventSubscriptions(RoutedEvent routedEvent) private IDisposable AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription)
{ {
if (!EventHandlers.TryGetValue(routedEvent, out var subscriptions)) _eventHandlers ??= new Dictionary<RoutedEvent, List<EventSubscription>>();
if (!_eventHandlers.TryGetValue(routedEvent, out var subscriptions))
{ {
subscriptions = new List<EventSubscription>(); subscriptions = new List<EventSubscription>();
EventHandlers.Add(routedEvent, subscriptions); _eventHandlers.Add(routedEvent, subscriptions);
} }
return subscriptions; subscriptions.Add(subscription);
return new UnsubscribeDisposable(subscriptions, subscription);
} }
private IDisposable AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription) private readonly struct EventSubscription
{ {
List<EventSubscription> subscriptions = GetEventSubscriptions(routedEvent); public EventSubscription(
Delegate handler,
RoutingStrategies routes,
bool handledEventsToo,
Action<Delegate, object, RoutedEventArgs>? invokeAdapter = null)
{
Handler = handler;
Routes = routes;
HandledEventsToo = handledEventsToo;
InvokeAdapter = invokeAdapter;
}
subscriptions.Add(subscription); public Action<Delegate, object, RoutedEventArgs>? InvokeAdapter { get; }
return new UnsubscribeDisposable(subscriptions, subscription); public Delegate Handler { get; }
public RoutingStrategies Routes { get; }
public bool HandledEventsToo { get; }
} }
private sealed class UnsubscribeDisposable : IDisposable private sealed class UnsubscribeDisposable : IDisposable
@ -261,67 +242,5 @@ namespace Avalonia.Interactivity
_subscriptions.Remove(_subscription); _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);
IInteractive parent = target.InteractiveParent;
if (parent != null)
{
Traverse(parent);
}
_postTraverse.Execute(target, _args);
}
}
} }
} }

5
src/Avalonia.Interactivity/InteractiveExtensions.cs

@ -2,8 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
namespace Avalonia.Interactivity namespace Avalonia.Interactivity
@ -30,6 +28,9 @@ namespace Avalonia.Interactivity
bool handledEventsToo = false) bool handledEventsToo = false)
where TEventArgs : RoutedEventArgs where TEventArgs : RoutedEventArgs
{ {
o = o ?? throw new ArgumentNullException(nameof(o));
routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
return Observable.Create<TEventArgs>(x => o.AddHandler( return Observable.Create<TEventArgs>(x => o.AddHandler(
routedEvent, routedEvent,
(_, e) => x.OnNext(e), (_, e) => x.OnNext(e),

20
src/Avalonia.Interactivity/RoutedEvent.cs

@ -25,10 +25,14 @@ namespace Avalonia.Interactivity
Type eventArgsType, Type eventArgsType,
Type ownerType) Type ownerType)
{ {
Contract.Requires<ArgumentNullException>(name != null); name = name ?? throw new ArgumentNullException(nameof(name));
Contract.Requires<ArgumentNullException>(eventArgsType != null); eventArgsType = eventArgsType ?? throw new ArgumentNullException(nameof(name));
Contract.Requires<ArgumentNullException>(ownerType != null); ownerType = ownerType ?? throw new ArgumentNullException(nameof(name));
Contract.Requires<InvalidCastException>(typeof(RoutedEventArgs).IsAssignableFrom(eventArgsType));
if (!typeof(RoutedEventArgs).IsAssignableFrom(eventArgsType))
{
throw new InvalidCastException("eventArgsType must be derived from RoutedEventArgs.");
}
EventArgsType = eventArgsType; EventArgsType = eventArgsType;
Name = name; Name = name;
@ -44,6 +48,8 @@ namespace Avalonia.Interactivity
public RoutingStrategies RoutingStrategies { get; } public RoutingStrategies RoutingStrategies { get; }
public bool HasRaisedSubscriptions => _raised.HasObservers;
public IObservable<(object, RoutedEventArgs)> Raised => _raised; public IObservable<(object, RoutedEventArgs)> Raised => _raised;
public IObservable<RoutedEventArgs> RouteFinished => _routeFinished; public IObservable<RoutedEventArgs> RouteFinished => _routeFinished;
@ -52,7 +58,7 @@ namespace Avalonia.Interactivity
RoutingStrategies routingStrategy) RoutingStrategies routingStrategy)
where TEventArgs : RoutedEventArgs where TEventArgs : RoutedEventArgs
{ {
Contract.Requires<ArgumentNullException>(name != null); name = name ?? throw new ArgumentNullException(nameof(name));
var routedEvent = new RoutedEvent<TEventArgs>(name, routingStrategy, typeof(TOwner)); var routedEvent = new RoutedEvent<TEventArgs>(name, routingStrategy, typeof(TOwner));
RoutedEventRegistry.Instance.Register(typeof(TOwner), routedEvent); RoutedEventRegistry.Instance.Register(typeof(TOwner), routedEvent);
@ -65,7 +71,7 @@ namespace Avalonia.Interactivity
Type ownerType) Type ownerType)
where TEventArgs : RoutedEventArgs where TEventArgs : RoutedEventArgs
{ {
Contract.Requires<ArgumentNullException>(name != null); name = name ?? throw new ArgumentNullException(nameof(name));
var routedEvent = new RoutedEvent<TEventArgs>(name, routingStrategy, ownerType); var routedEvent = new RoutedEvent<TEventArgs>(name, routingStrategy, ownerType);
RoutedEventRegistry.Instance.Register(ownerType, routedEvent); RoutedEventRegistry.Instance.Register(ownerType, routedEvent);
@ -108,8 +114,6 @@ namespace Avalonia.Interactivity
public RoutedEvent(string name, RoutingStrategies routingStrategies, Type ownerType) public RoutedEvent(string name, RoutingStrategies routingStrategies, Type ownerType)
: base(name, routingStrategies, typeof(TEventArgs), ownerType) : base(name, routingStrategies, typeof(TEventArgs), ownerType)
{ {
Contract.Requires<ArgumentNullException>(name != null);
Contract.Requires<ArgumentNullException>(ownerType != null);
} }
[Obsolete("Use overload taking Action<TTarget, TEventArgs>.")] [Obsolete("Use overload taking Action<TTarget, TEventArgs>.")]

8
src/Avalonia.Interactivity/RoutedEventArgs.cs

@ -11,12 +11,12 @@ namespace Avalonia.Interactivity
{ {
} }
public RoutedEventArgs(RoutedEvent routedEvent) public RoutedEventArgs(RoutedEvent? routedEvent)
{ {
RoutedEvent = routedEvent; RoutedEvent = routedEvent;
} }
public RoutedEventArgs(RoutedEvent routedEvent, IInteractive source) public RoutedEventArgs(RoutedEvent? routedEvent, IInteractive? source)
{ {
RoutedEvent = routedEvent; RoutedEvent = routedEvent;
Source = source; Source = source;
@ -24,10 +24,10 @@ namespace Avalonia.Interactivity
public bool Handled { get; set; } public bool Handled { get; set; }
public RoutedEvent RoutedEvent { get; set; } public RoutedEvent? RoutedEvent { get; set; }
public RoutingStrategies Route { get; set; } public RoutingStrategies Route { get; set; }
public IInteractive Source { get; set; } public IInteractive? Source { get; set; }
} }
} }

6
src/Avalonia.Interactivity/RoutedEventRegistry.cs

@ -32,8 +32,8 @@ namespace Avalonia.Interactivity
/// </remarks> /// </remarks>
public void Register(Type type, RoutedEvent @event) public void Register(Type type, RoutedEvent @event)
{ {
Contract.Requires<ArgumentNullException>(type != null); type = type ?? throw new ArgumentNullException(nameof(type));
Contract.Requires<ArgumentNullException>(@event != null); @event = @event ?? throw new ArgumentNullException(nameof(@event));
if (!_registeredRoutedEvents.TryGetValue(type, out var list)) if (!_registeredRoutedEvents.TryGetValue(type, out var list))
{ {
@ -66,7 +66,7 @@ namespace Avalonia.Interactivity
/// <returns>All routed events registered with the provided type.</returns> /// <returns>All routed events registered with the provided type.</returns>
public IReadOnlyList<RoutedEvent> GetRegistered(Type type) public IReadOnlyList<RoutedEvent> GetRegistered(Type type)
{ {
Contract.Requires<ArgumentNullException>(type != null); type = type ?? throw new ArgumentNullException(nameof(type));
if (_registeredRoutedEvents.TryGetValue(type, out var events)) if (_registeredRoutedEvents.TryGetValue(type, out var events))
{ {

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

@ -4,7 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Interactivity;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Xunit; using Xunit;
@ -358,6 +357,29 @@ namespace Avalonia.Interactivity.UnitTests
Assert.Equal(1, called); Assert.Equal(1, called);
} }
[Fact]
public void Removing_Control_In_Handler_Should_Not_Stop_Event()
{
// Issue #3176
var ev = new RoutedEvent("test", RoutingStrategies.Bubble, typeof(RoutedEventArgs), typeof(TestInteractive));
var invoked = new List<string>();
EventHandler<RoutedEventArgs> handler = (s, e) => invoked.Add(((TestInteractive)s).Name);
var parent = CreateTree(ev, handler, RoutingStrategies.Bubble | RoutingStrategies.Tunnel);
var target = (IInteractive)parent.GetVisualChildren().Single();
EventHandler<RoutedEventArgs> removeHandler = (s, e) =>
{
parent.Children = Array.Empty<IVisual>();
};
target.AddHandler(ev, removeHandler);
var args = new RoutedEventArgs(ev, target);
target.RaiseEvent(args);
Assert.Equal(new[] { "3", "2b", "1" }, invoked);
}
private TestInteractive CreateTree( private TestInteractive CreateTree(
RoutedEvent ev, RoutedEvent ev,
EventHandler<RoutedEventArgs> handler, EventHandler<RoutedEventArgs> handler,
@ -414,6 +436,7 @@ namespace Avalonia.Interactivity.UnitTests
set set
{ {
VisualChildren.Clear();
VisualChildren.AddRange(value.Cast<Visual>()); VisualChildren.AddRange(value.Cast<Visual>());
} }
} }

2
tests/Avalonia.UnitTests/MouseTestHelper.cs

@ -56,7 +56,7 @@ namespace Avalonia.UnitTests
{ {
_pressedButton = mouseButton; _pressedButton = mouseButton;
_pointer.Capture((IInputElement)target); _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)); GetModifiers(modifiers), clickCount));
} }
} }

Loading…
Cancel
Save