16 changed files with 421 additions and 117 deletions
@ -0,0 +1,12 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.Utilities; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines a listener to a event subscribed vis the <see cref="WeakEvent{TTarget, TEventArgs}"/>.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TEventArgs">The type of the event arguments.</typeparam>
|
||||
|
public interface IWeakEventSubscriber<in TEventArgs> where TEventArgs : EventArgs |
||||
|
{ |
||||
|
void OnEvent(object? sender, WeakEvent ev, TEventArgs e); |
||||
|
} |
||||
@ -0,0 +1,175 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Reflection; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
|
||||
|
namespace Avalonia.Utilities; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Manages subscriptions to events using weak listeners.
|
||||
|
/// </summary>
|
||||
|
public class WeakEvent<TSender, TEventArgs> : WeakEvent where TEventArgs : EventArgs where TSender : class |
||||
|
{ |
||||
|
private readonly Func<TSender, EventHandler<TEventArgs>, Action> _subscribe; |
||||
|
|
||||
|
readonly ConditionalWeakTable<object, Subscription> _subscriptions = new(); |
||||
|
|
||||
|
internal WeakEvent( |
||||
|
Action<TSender, EventHandler<TEventArgs>> subscribe, |
||||
|
Action<TSender, EventHandler<TEventArgs>> unsubscribe) |
||||
|
{ |
||||
|
_subscribe = (t, s) => |
||||
|
{ |
||||
|
subscribe(t, s); |
||||
|
return () => unsubscribe(t, s); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
internal WeakEvent(Func<TSender, EventHandler<TEventArgs>, Action> subscribe) |
||||
|
{ |
||||
|
_subscribe = subscribe; |
||||
|
} |
||||
|
|
||||
|
public void Subscribe(TSender target, IWeakEventSubscriber<TEventArgs> subscriber) |
||||
|
{ |
||||
|
if (!_subscriptions.TryGetValue(target, out var subscription)) |
||||
|
_subscriptions.Add(target, subscription = new Subscription(this, target)); |
||||
|
subscription.Add(new WeakReference<IWeakEventSubscriber<TEventArgs>>(subscriber)); |
||||
|
} |
||||
|
|
||||
|
public void Unsubscribe(TSender target, IWeakEventSubscriber<TEventArgs> subscriber) |
||||
|
{ |
||||
|
if (_subscriptions.TryGetValue(target, out var subscription)) |
||||
|
subscription.Remove(subscriber); |
||||
|
} |
||||
|
|
||||
|
private class Subscription |
||||
|
{ |
||||
|
private readonly WeakEvent<TSender, TEventArgs> _ev; |
||||
|
private readonly TSender _target; |
||||
|
|
||||
|
private WeakReference<IWeakEventSubscriber<TEventArgs>>?[] _data = |
||||
|
new WeakReference<IWeakEventSubscriber<TEventArgs>>[16]; |
||||
|
private int _count; |
||||
|
private readonly Action _unsubscribe; |
||||
|
|
||||
|
public Subscription(WeakEvent<TSender, TEventArgs> ev, TSender target) |
||||
|
{ |
||||
|
_ev = ev; |
||||
|
_target = target; |
||||
|
|
||||
|
_unsubscribe = ev._subscribe(target, OnEvent); |
||||
|
} |
||||
|
|
||||
|
void Destroy() |
||||
|
{ |
||||
|
_unsubscribe(); |
||||
|
_ev._subscriptions.Remove(_target); |
||||
|
} |
||||
|
|
||||
|
public void Add(WeakReference<IWeakEventSubscriber<TEventArgs>> s) |
||||
|
{ |
||||
|
if (_count == _data.Length) |
||||
|
{ |
||||
|
//Extend capacity
|
||||
|
var extendedData = new WeakReference<IWeakEventSubscriber<TEventArgs>>?[_data.Length * 2]; |
||||
|
Array.Copy(_data, extendedData, _data.Length); |
||||
|
_data = extendedData; |
||||
|
} |
||||
|
|
||||
|
_data[_count] = s; |
||||
|
_count++; |
||||
|
} |
||||
|
|
||||
|
public void Remove(IWeakEventSubscriber<TEventArgs> s) |
||||
|
{ |
||||
|
var removed = false; |
||||
|
|
||||
|
for (int c = 0; c < _count; ++c) |
||||
|
{ |
||||
|
var reference = _data[c]; |
||||
|
|
||||
|
if (reference != null && reference.TryGetTarget(out var instance) && instance == s) |
||||
|
{ |
||||
|
_data[c] = null; |
||||
|
removed = true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (removed) |
||||
|
{ |
||||
|
Compact(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void Compact() |
||||
|
{ |
||||
|
int empty = -1; |
||||
|
for (var c = 0; c < _count; c++) |
||||
|
{ |
||||
|
var r = _data[c]; |
||||
|
//Mark current index as first empty
|
||||
|
if (r == null && empty == -1) |
||||
|
empty = c; |
||||
|
//If current element isn't null and we have an empty one
|
||||
|
if (r != null && empty != -1) |
||||
|
{ |
||||
|
_data[c] = null; |
||||
|
_data[empty] = r; |
||||
|
empty++; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (empty != -1) |
||||
|
_count = empty; |
||||
|
if (_count == 0) |
||||
|
Destroy(); |
||||
|
} |
||||
|
|
||||
|
void OnEvent(object? sender, TEventArgs eventArgs) |
||||
|
{ |
||||
|
var needCompact = false; |
||||
|
for (var c = 0; c < _count; c++) |
||||
|
{ |
||||
|
var r = _data[c]; |
||||
|
if (r?.TryGetTarget(out var sub) == true) |
||||
|
sub!.OnEvent(_target, _ev, eventArgs); |
||||
|
else |
||||
|
needCompact = true; |
||||
|
} |
||||
|
|
||||
|
if (needCompact) |
||||
|
Compact(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public class WeakEvent |
||||
|
{ |
||||
|
public static WeakEvent<TSender, TEventArgs> Register<TSender, TEventArgs>( |
||||
|
Action<TSender, EventHandler<TEventArgs>> subscribe, |
||||
|
Action<TSender, EventHandler<TEventArgs>> unsubscribe) where TSender : class where TEventArgs : EventArgs |
||||
|
{ |
||||
|
return new WeakEvent<TSender, TEventArgs>(subscribe, unsubscribe); |
||||
|
} |
||||
|
|
||||
|
public static WeakEvent<TSender, TEventArgs> Register<TSender, TEventArgs>( |
||||
|
Func<TSender, EventHandler<TEventArgs>, Action> subscribe) where TSender : class where TEventArgs : EventArgs |
||||
|
{ |
||||
|
return new WeakEvent<TSender, TEventArgs>(subscribe); |
||||
|
} |
||||
|
|
||||
|
public static WeakEvent<TSender, EventArgs> Register<TSender>( |
||||
|
Action<TSender, EventHandler> subscribe, |
||||
|
Action<TSender, EventHandler> unsubscribe) where TSender : class |
||||
|
{ |
||||
|
return Register<TSender, EventArgs>((s, h) => |
||||
|
{ |
||||
|
EventHandler handler = (_, e) => h(s, e); |
||||
|
subscribe(s, handler); |
||||
|
return () => unsubscribe(s, handler); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Specialized; |
||||
|
using System.ComponentModel; |
||||
|
using System.Windows.Input; |
||||
|
|
||||
|
namespace Avalonia.Utilities; |
||||
|
|
||||
|
public class WeakEvents |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents CollectionChanged event from <see cref="INotifyCollectionChanged"/>
|
||||
|
/// </summary>
|
||||
|
public static readonly WeakEvent<INotifyCollectionChanged, NotifyCollectionChangedEventArgs> |
||||
|
CollectionChanged = WeakEvent.Register<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>( |
||||
|
(c, s) => |
||||
|
{ |
||||
|
NotifyCollectionChangedEventHandler handler = (_, e) => s(c, e); |
||||
|
c.CollectionChanged += handler; |
||||
|
return () => c.CollectionChanged -= handler; |
||||
|
}); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents PropertyChanged event from <see cref="INotifyPropertyChanged"/>
|
||||
|
/// </summary>
|
||||
|
public static readonly WeakEvent<INotifyPropertyChanged, PropertyChangedEventArgs> |
||||
|
PropertyChanged = WeakEvent.Register<INotifyPropertyChanged, PropertyChangedEventArgs>( |
||||
|
(s, h) => |
||||
|
{ |
||||
|
PropertyChangedEventHandler handler = (_, e) => h(s, e); |
||||
|
s.PropertyChanged += handler; |
||||
|
return () => s.PropertyChanged -= handler; |
||||
|
}); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents CanExecuteChanged event from <see cref="ICommand"/>
|
||||
|
/// </summary>
|
||||
|
public static readonly WeakEvent<ICommand, EventArgs> CommandCanExecuteChanged = |
||||
|
WeakEvent.Register<ICommand>((s, h) => s.CanExecuteChanged += h, |
||||
|
(s, h) => s.CanExecuteChanged -= h); |
||||
|
} |
||||
@ -0,0 +1,75 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Text; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Utilities; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Base.UnitTests |
||||
|
{ |
||||
|
public class WeakEventManagerTests |
||||
|
{ |
||||
|
class EventSource |
||||
|
{ |
||||
|
public event EventHandler<EventArgs> Event; |
||||
|
|
||||
|
public void Fire() |
||||
|
{ |
||||
|
Event?.Invoke(this, new EventArgs()); |
||||
|
} |
||||
|
|
||||
|
public static readonly WeakEvent<EventSource, EventArgs> WeakEvent = WeakEvent<EventSource, EventArgs>.Register( |
||||
|
(t, s) => t.Event += s, |
||||
|
(t, s) => t.Event -= s); |
||||
|
} |
||||
|
|
||||
|
class Subscriber : IWeakEventSubscriber<EventArgs> |
||||
|
{ |
||||
|
private readonly Action _onEvent; |
||||
|
|
||||
|
public Subscriber(Action onEvent) |
||||
|
{ |
||||
|
_onEvent = onEvent; |
||||
|
} |
||||
|
|
||||
|
public void OnEvent(object sender, WeakEvent ev, EventArgs args) |
||||
|
{ |
||||
|
_onEvent?.Invoke(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void EventShouldBePassedToSubscriber() |
||||
|
{ |
||||
|
bool handled = false; |
||||
|
var subscriber = new Subscriber(() => handled = true); |
||||
|
var source = new EventSource(); |
||||
|
EventSource.WeakEvent.Subscribe(source, subscriber); |
||||
|
|
||||
|
source.Fire(); |
||||
|
Assert.True(handled); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
[Fact] |
||||
|
public void EventHandlerShouldNotBeKeptAlive() |
||||
|
{ |
||||
|
bool handled = false; |
||||
|
var source = new EventSource(); |
||||
|
AddSubscriber(source, () => handled = true); |
||||
|
for (int c = 0; c < 10; c++) |
||||
|
{ |
||||
|
GC.Collect(); |
||||
|
GC.Collect(3, GCCollectionMode.Forced, true); |
||||
|
} |
||||
|
source.Fire(); |
||||
|
Assert.False(handled); |
||||
|
} |
||||
|
|
||||
|
private void AddSubscriber(EventSource source, Action func) |
||||
|
{ |
||||
|
EventSource.WeakEvent.Subscribe(source, new Subscriber(func)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue