Browse Source

Merge branch 'master' into feature/menuitem-inputgesturetext

pull/3602/head
Dariusz Komosiński 6 years ago
committed by GitHub
parent
commit
2dcd0f2030
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">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Nullable>Enable</Nullable>
<WarningsAsErrors>CS8600;CS8602;CS8603</WarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />
@ -9,6 +11,4 @@
<ProjectReference Include="..\Avalonia.Visuals\Avalonia.Visuals.csproj" />
</ItemGroup>
<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>
/// Gets the interactive parent of the object for bubbling and tunneling events.
/// </summary>
IInteractive InteractiveParent { get; }
IInteractive? InteractiveParent { get; }
/// <summary>
/// Adds a handler for the specified routed event.
@ -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>

283
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;
@ -15,16 +13,12 @@ namespace Avalonia.Interactivity
/// </summary>
public class Interactive : Layoutable, IInteractive
{
private Dictionary<RoutedEvent, List<EventSubscription>> _eventHandlers;
private static readonly Dictionary<Type, HandlerInvokeSignature> s_invokeHandlerCache = new Dictionary<Type, HandlerInvokeSignature>();
private Dictionary<RoutedEvent, List<EventSubscription>>? _eventHandlers;
/// <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 ?? (_eventHandlers = new Dictionary<RoutedEvent, List<EventSubscription>>());
IInteractive? IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive;
/// <summary>
/// Adds a handler for the specified routed event.
@ -40,16 +34,10 @@ namespace Avalonia.Interactivity
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
bool handledEventsToo = false)
{
Contract.Requires<ArgumentNullException>(routedEvent != null);
Contract.Requires<ArgumentNullException>(handler != null);
var subscription = new EventSubscription
{
Handler = handler,
Routes = routes,
AlsoIfHandled = handledEventsToo,
};
routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
handler = handler ?? throw new ArgumentNullException(nameof(handler));
var subscription = new EventSubscription(handler, routes, handledEventsToo);
return AddEventSubscription(routedEvent, subscription);
}
@ -68,35 +56,18 @@ namespace Avalonia.Interactivity
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
bool handledEventsToo = false) where TEventArgs : RoutedEventArgs
{
Contract.Requires<ArgumentNullException>(routedEvent != null);
Contract.Requires<ArgumentNullException>(handler != null);
// 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;
routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
handler = handler ?? throw new ArgumentNullException(nameof(handler));
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);
}
invokeAdapter = InvokeAdapter;
s_invokeHandlerCache.Add(eventArgsType, invokeAdapter);
typedHandler(sender, typedArgs);
}
var subscription = new EventSubscription
{
InvokeAdapter = invokeAdapter,
Handler = handler,
Routes = routes,
AlsoIfHandled = handledEventsToo,
};
var subscription = new EventSubscription(handler, routes, handledEventsToo, (baseHandler, sender, args) => InvokeAdapter(baseHandler, sender, args));
return AddEventSubscription(routedEvent, subscription);
}
@ -108,14 +79,19 @@ namespace Avalonia.Interactivity
/// <param name="handler">The handler.</param>
public void RemoveHandler(RoutedEvent routedEvent, Delegate handler)
{
Contract.Requires<ArgumentNullException>(routedEvent != null);
Contract.Requires<ArgumentNullException>(handler != null);
List<EventSubscription> subscriptions = null;
routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
handler = handler ?? throw new ArgumentNullException(nameof(handler));
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>
public void RaiseEvent(RoutedEventArgs e)
{
Contract.Requires<ArgumentNullException>(e != null);
e.Source = e.Source ?? this;
if (e.RoutedEvent.RoutingStrategies == RoutingStrategies.Direct)
{
e.Route = RoutingStrategies.Direct;
RaiseEventImpl(e);
e.RoutedEvent.InvokeRouteFinished(e);
}
e = e ?? throw new ArgumentNullException(nameof(e));
if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Tunnel) != 0)
if (e.RoutedEvent == null)
{
TunnelEvent(e);
e.RoutedEvent.InvokeRouteFinished(e);
throw new ArgumentException("Cannot raise an event whose RoutedEvent is null.");
}
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)
{
Contract.Requires<ArgumentNullException>(e != null);
routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
route = route ?? throw new ArgumentNullException(nameof(route));
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)
{
Contract.Requires<ArgumentNullException>(e != null);
e.Route = RoutingStrategies.Tunnel;
var traverser = HierarchyTraverser<NopTraverse, RaiseEventTraverse>.Create(e);
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)
{
Contract.Requires<ArgumentNullException>(e != null);
e.RoutedEvent.InvokeRaised(this, e);
e = e ?? throw new ArgumentNullException(nameof(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())
{
bool correctRoute = (e.Route & sub.Routes) != 0;
bool notFinished = !e.Handled || sub.AlsoIfHandled;
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;
}
}
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>();
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
@ -261,67 +242,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);
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.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
namespace Avalonia.Interactivity
@ -30,6 +28,9 @@ namespace Avalonia.Interactivity
bool handledEventsToo = false)
where TEventArgs : RoutedEventArgs
{
o = o ?? throw new ArgumentNullException(nameof(o));
routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
return Observable.Create<TEventArgs>(x => o.AddHandler(
routedEvent,
(_, e) => x.OnNext(e),

20
src/Avalonia.Interactivity/RoutedEvent.cs

@ -25,10 +25,14 @@ namespace Avalonia.Interactivity
Type eventArgsType,
Type ownerType)
{
Contract.Requires<ArgumentNullException>(name != null);
Contract.Requires<ArgumentNullException>(eventArgsType != null);
Contract.Requires<ArgumentNullException>(ownerType != null);
Contract.Requires<InvalidCastException>(typeof(RoutedEventArgs).IsAssignableFrom(eventArgsType));
name = name ?? throw new ArgumentNullException(nameof(name));
eventArgsType = eventArgsType ?? throw new ArgumentNullException(nameof(name));
ownerType = ownerType ?? throw new ArgumentNullException(nameof(name));
if (!typeof(RoutedEventArgs).IsAssignableFrom(eventArgsType))
{
throw new InvalidCastException("eventArgsType must be derived from RoutedEventArgs.");
}
EventArgsType = eventArgsType;
Name = name;
@ -44,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;
@ -52,7 +58,7 @@ namespace Avalonia.Interactivity
RoutingStrategies routingStrategy)
where TEventArgs : RoutedEventArgs
{
Contract.Requires<ArgumentNullException>(name != null);
name = name ?? throw new ArgumentNullException(nameof(name));
var routedEvent = new RoutedEvent<TEventArgs>(name, routingStrategy, typeof(TOwner));
RoutedEventRegistry.Instance.Register(typeof(TOwner), routedEvent);
@ -65,7 +71,7 @@ namespace Avalonia.Interactivity
Type ownerType)
where TEventArgs : RoutedEventArgs
{
Contract.Requires<ArgumentNullException>(name != null);
name = name ?? throw new ArgumentNullException(nameof(name));
var routedEvent = new RoutedEvent<TEventArgs>(name, routingStrategy, ownerType);
RoutedEventRegistry.Instance.Register(ownerType, routedEvent);
@ -108,8 +114,6 @@ namespace Avalonia.Interactivity
public RoutedEvent(string name, RoutingStrategies routingStrategies, Type ownerType)
: base(name, routingStrategies, typeof(TEventArgs), ownerType)
{
Contract.Requires<ArgumentNullException>(name != null);
Contract.Requires<ArgumentNullException>(ownerType != null);
}
[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;
}
public RoutedEventArgs(RoutedEvent routedEvent, IInteractive source)
public RoutedEventArgs(RoutedEvent? routedEvent, IInteractive? source)
{
RoutedEvent = routedEvent;
Source = source;
@ -24,10 +24,10 @@ namespace Avalonia.Interactivity
public bool Handled { get; set; }
public RoutedEvent RoutedEvent { get; set; }
public RoutedEvent? RoutedEvent { 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>
public void Register(Type type, RoutedEvent @event)
{
Contract.Requires<ArgumentNullException>(type != null);
Contract.Requires<ArgumentNullException>(@event != null);
type = type ?? throw new ArgumentNullException(nameof(type));
@event = @event ?? throw new ArgumentNullException(nameof(@event));
if (!_registeredRoutedEvents.TryGetValue(type, out var list))
{
@ -66,7 +66,7 @@ namespace Avalonia.Interactivity
/// <returns>All routed events registered with the provided type.</returns>
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))
{

25
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;
@ -358,6 +357,29 @@ namespace Avalonia.Interactivity.UnitTests
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(
RoutedEvent ev,
EventHandler<RoutedEventArgs> handler,
@ -414,6 +436,7 @@ namespace Avalonia.Interactivity.UnitTests
set
{
VisualChildren.Clear();
VisualChildren.AddRange(value.Cast<Visual>());
}
}

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