committed by
GitHub
93 changed files with 1349 additions and 433 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,187 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Reflection; |
|||
using System.Runtime.CompilerServices; |
|||
using Avalonia.Threading; |
|||
|
|||
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 readonly Action _compact; |
|||
|
|||
private WeakReference<IWeakEventSubscriber<TEventArgs>>?[] _data = |
|||
new WeakReference<IWeakEventSubscriber<TEventArgs>>[16]; |
|||
private int _count; |
|||
private readonly Action _unsubscribe; |
|||
private bool _compactScheduled; |
|||
|
|||
public Subscription(WeakEvent<TSender, TEventArgs> ev, TSender target) |
|||
{ |
|||
_ev = ev; |
|||
_target = target; |
|||
_compact = Compact; |
|||
_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) |
|||
{ |
|||
ScheduleCompact(); |
|||
} |
|||
} |
|||
|
|||
void ScheduleCompact() |
|||
{ |
|||
if(_compactScheduled) |
|||
return; |
|||
_compactScheduled = true; |
|||
Dispatcher.UIThread.Post(_compact, DispatcherPriority.Background); |
|||
} |
|||
|
|||
void Compact() |
|||
{ |
|||
_compactScheduled = false; |
|||
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) |
|||
ScheduleCompact(); |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
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,326 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Controls.Primitives.PopupPositioning; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Raw; |
|||
using Avalonia.Input.TextInput; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering; |
|||
|
|||
namespace Avalonia.Controls; |
|||
|
|||
internal class ValidatingToplevelImpl : ITopLevelImpl, ITopLevelImplWithNativeControlHost, |
|||
ITopLevelImplWithNativeMenuExporter, ITopLevelImplWithTextInputMethod |
|||
{ |
|||
private readonly ITopLevelImpl _impl; |
|||
private bool _disposed; |
|||
|
|||
public ValidatingToplevelImpl(ITopLevelImpl impl) |
|||
{ |
|||
_impl = impl ?? throw new InvalidOperationException( |
|||
"Could not create TopLevel implementation: maybe no windowing subsystem was initialized?"); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_disposed = true; |
|||
_impl.Dispose(); |
|||
} |
|||
|
|||
protected void CheckDisposed() |
|||
{ |
|||
if (_disposed) |
|||
throw new ObjectDisposedException(_impl.GetType().FullName); |
|||
} |
|||
|
|||
protected ITopLevelImpl Inner |
|||
{ |
|||
get |
|||
{ |
|||
CheckDisposed(); |
|||
return _impl; |
|||
} |
|||
} |
|||
|
|||
public static ITopLevelImpl Wrap(ITopLevelImpl impl) |
|||
{ |
|||
#if DEBUG
|
|||
if (impl is ValidatingToplevelImpl) |
|||
return impl; |
|||
return new ValidatingToplevelImpl(impl); |
|||
#else
|
|||
return impl; |
|||
#endif
|
|||
} |
|||
|
|||
public Size ClientSize => Inner.ClientSize; |
|||
public Size? FrameSize => Inner.FrameSize; |
|||
public double RenderScaling => Inner.RenderScaling; |
|||
public IEnumerable<object> Surfaces => Inner.Surfaces; |
|||
|
|||
public Action<RawInputEventArgs> Input |
|||
{ |
|||
get => Inner.Input; |
|||
set => Inner.Input = value; |
|||
} |
|||
|
|||
public Action<Rect> Paint |
|||
{ |
|||
get => Inner.Paint; |
|||
set => Inner.Paint = value; |
|||
} |
|||
|
|||
public Action<Size, PlatformResizeReason> Resized |
|||
{ |
|||
get => Inner.Resized; |
|||
set => Inner.Resized = value; |
|||
} |
|||
|
|||
public Action<double> ScalingChanged |
|||
{ |
|||
get => Inner.ScalingChanged; |
|||
set => Inner.ScalingChanged = value; |
|||
} |
|||
|
|||
public Action<WindowTransparencyLevel> TransparencyLevelChanged |
|||
{ |
|||
get => Inner.TransparencyLevelChanged; |
|||
set => Inner.TransparencyLevelChanged = value; |
|||
} |
|||
|
|||
public IRenderer CreateRenderer(IRenderRoot root) => Inner.CreateRenderer(root); |
|||
|
|||
public void Invalidate(Rect rect) => Inner.Invalidate(rect); |
|||
|
|||
public void SetInputRoot(IInputRoot inputRoot) => Inner.SetInputRoot(inputRoot); |
|||
|
|||
public Point PointToClient(PixelPoint point) => Inner.PointToClient(point); |
|||
|
|||
public PixelPoint PointToScreen(Point point) => Inner.PointToScreen(point); |
|||
|
|||
public void SetCursor(ICursorImpl cursor) => Inner.SetCursor(cursor); |
|||
|
|||
public Action Closed |
|||
{ |
|||
get => Inner.Closed; |
|||
set => Inner.Closed = value; |
|||
} |
|||
|
|||
public Action LostFocus |
|||
{ |
|||
get => Inner.LostFocus; |
|||
set => Inner.LostFocus = value; |
|||
} |
|||
|
|||
// Exception: for some reason we are notifying platform mouse device from TopLevel.cs
|
|||
public IMouseDevice MouseDevice => _impl.MouseDevice; |
|||
public IPopupImpl CreatePopup() => Inner.CreatePopup(); |
|||
|
|||
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) => |
|||
Inner.SetTransparencyLevelHint(transparencyLevel); |
|||
|
|||
|
|||
public WindowTransparencyLevel TransparencyLevel => Inner.TransparencyLevel; |
|||
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => Inner.AcrylicCompensationLevels; |
|||
public INativeControlHostImpl NativeControlHost => (Inner as ITopLevelImplWithNativeControlHost)?.NativeControlHost; |
|||
|
|||
public ITopLevelNativeMenuExporter NativeMenuExporter => |
|||
(Inner as ITopLevelImplWithNativeMenuExporter)?.NativeMenuExporter; |
|||
|
|||
public ITextInputMethodImpl TextInputMethod => (Inner as ITopLevelImplWithTextInputMethod)?.TextInputMethod; |
|||
} |
|||
|
|||
internal class ValidatingWindowBaseImpl : ValidatingToplevelImpl, IWindowBaseImpl |
|||
{ |
|||
private readonly IWindowBaseImpl _impl; |
|||
|
|||
public ValidatingWindowBaseImpl(IWindowBaseImpl impl) : base(impl) |
|||
{ |
|||
_impl = impl; |
|||
} |
|||
|
|||
protected new IWindowBaseImpl Inner |
|||
{ |
|||
get |
|||
{ |
|||
CheckDisposed(); |
|||
return _impl; |
|||
} |
|||
} |
|||
|
|||
public static IWindowBaseImpl Wrap(IWindowBaseImpl impl) |
|||
{ |
|||
#if DEBUG
|
|||
if (impl is ValidatingToplevelImpl) |
|||
return impl; |
|||
return new ValidatingWindowBaseImpl(impl); |
|||
#else
|
|||
return impl; |
|||
#endif
|
|||
} |
|||
|
|||
public void Show(bool activate, bool isDialog) => Inner.Show(activate, isDialog); |
|||
|
|||
public void Hide() => Inner.Hide(); |
|||
|
|||
public double DesktopScaling => Inner.DesktopScaling; |
|||
public PixelPoint Position => Inner.Position; |
|||
|
|||
public Action<PixelPoint> PositionChanged |
|||
{ |
|||
get => Inner.PositionChanged; |
|||
set => Inner.PositionChanged = value; |
|||
} |
|||
|
|||
public void Activate() => Inner.Activate(); |
|||
|
|||
public Action Deactivated |
|||
{ |
|||
get => Inner.Deactivated; |
|||
set => Inner.Deactivated = value; |
|||
} |
|||
|
|||
public Action Activated |
|||
{ |
|||
get => Inner.Activated; |
|||
set => Inner.Deactivated = value; |
|||
} |
|||
|
|||
public IPlatformHandle Handle => Inner.Handle; |
|||
public Size MaxAutoSizeHint => Inner.MaxAutoSizeHint; |
|||
public void SetTopmost(bool value) => Inner.SetTopmost(value); |
|||
public IScreenImpl Screen => Inner.Screen; |
|||
} |
|||
|
|||
internal class ValidatingWindowImpl : ValidatingWindowBaseImpl, IWindowImpl |
|||
{ |
|||
private readonly IWindowImpl _impl; |
|||
|
|||
public ValidatingWindowImpl(IWindowImpl impl) : base(impl) |
|||
{ |
|||
_impl = impl; |
|||
} |
|||
|
|||
protected new IWindowImpl Inner |
|||
{ |
|||
get |
|||
{ |
|||
CheckDisposed(); |
|||
return _impl; |
|||
} |
|||
} |
|||
|
|||
public static IWindowImpl Wrap(IWindowImpl impl) |
|||
{ |
|||
#if DEBUG
|
|||
if (impl is ValidatingToplevelImpl) |
|||
return impl; |
|||
return new ValidatingWindowImpl(impl); |
|||
#else
|
|||
return impl; |
|||
#endif
|
|||
} |
|||
|
|||
public WindowState WindowState |
|||
{ |
|||
get => Inner.WindowState; |
|||
set => Inner.WindowState = value; |
|||
} |
|||
|
|||
public Action<WindowState> WindowStateChanged |
|||
{ |
|||
get => Inner.WindowStateChanged; |
|||
set => Inner.WindowStateChanged = value; |
|||
} |
|||
|
|||
public void SetTitle(string title) => Inner.SetTitle(title); |
|||
|
|||
public void SetParent(IWindowImpl parent) => Inner.SetParent(parent); |
|||
|
|||
public void SetEnabled(bool enable) => Inner.SetEnabled(enable); |
|||
|
|||
public Action GotInputWhenDisabled |
|||
{ |
|||
get => Inner.GotInputWhenDisabled; |
|||
set => Inner.GotInputWhenDisabled = value; |
|||
} |
|||
|
|||
public void SetSystemDecorations(SystemDecorations enabled) => Inner.SetSystemDecorations(enabled); |
|||
|
|||
public void SetIcon(IWindowIconImpl icon) => Inner.SetIcon(icon); |
|||
|
|||
public void ShowTaskbarIcon(bool value) => Inner.ShowTaskbarIcon(value); |
|||
|
|||
public void CanResize(bool value) => Inner.CanResize(value); |
|||
|
|||
public Func<bool> Closing |
|||
{ |
|||
get => Inner.Closing; |
|||
set => Inner.Closing = value; |
|||
} |
|||
|
|||
public bool IsClientAreaExtendedToDecorations => Inner.IsClientAreaExtendedToDecorations; |
|||
|
|||
public Action<bool> ExtendClientAreaToDecorationsChanged |
|||
{ |
|||
get => Inner.ExtendClientAreaToDecorationsChanged; |
|||
set => Inner.ExtendClientAreaToDecorationsChanged = value; |
|||
} |
|||
|
|||
public bool NeedsManagedDecorations => Inner.NeedsManagedDecorations; |
|||
public Thickness ExtendedMargins => Inner.ExtendedMargins; |
|||
public Thickness OffScreenMargin => Inner.OffScreenMargin; |
|||
public void BeginMoveDrag(PointerPressedEventArgs e) => Inner.BeginMoveDrag(e); |
|||
|
|||
public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) => Inner.BeginResizeDrag(edge, e); |
|||
|
|||
public void Resize(Size clientSize, PlatformResizeReason reason) => |
|||
Inner.Resize(clientSize, reason); |
|||
|
|||
public void Move(PixelPoint point) => Inner.Move(point); |
|||
|
|||
public void SetMinMaxSize(Size minSize, Size maxSize) => Inner.SetMinMaxSize(minSize, maxSize); |
|||
|
|||
public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint) => |
|||
Inner.SetExtendClientAreaToDecorationsHint(extendIntoClientAreaHint); |
|||
|
|||
public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) => |
|||
Inner.SetExtendClientAreaChromeHints(hints); |
|||
|
|||
public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) => |
|||
Inner.SetExtendClientAreaTitleBarHeightHint(titleBarHeight); |
|||
} |
|||
|
|||
internal class ValidatingPopupImpl : ValidatingWindowBaseImpl, IPopupImpl |
|||
{ |
|||
private readonly IPopupImpl _impl; |
|||
|
|||
public ValidatingPopupImpl(IPopupImpl impl) : base(impl) |
|||
{ |
|||
_impl = impl; |
|||
} |
|||
|
|||
protected new IPopupImpl Inner |
|||
{ |
|||
get |
|||
{ |
|||
CheckDisposed(); |
|||
return _impl; |
|||
} |
|||
} |
|||
|
|||
public static IPopupImpl Wrap(IPopupImpl impl) |
|||
{ |
|||
#if DEBUG
|
|||
if (impl is ValidatingToplevelImpl) |
|||
return impl; |
|||
return new ValidatingPopupImpl(impl); |
|||
#else
|
|||
return impl; |
|||
#endif
|
|||
} |
|||
|
|||
public IPopupPositioner PopupPositioner => Inner.PopupPositioner; |
|||
public void SetWindowManagerAddShadowHint(bool enabled) => Inner.SetWindowManagerAddShadowHint(enabled); |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
Compat issues with assembly Avalonia.Dialogs: |
|||
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Dialogs.AboutAvaloniaDialog' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract. |
|||
Total Issues: 1 |
|||
@ -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 WeakEventTests |
|||
{ |
|||
class EventSource |
|||
{ |
|||
public event EventHandler Event; |
|||
|
|||
public void Fire() |
|||
{ |
|||
Event?.Invoke(this, new EventArgs()); |
|||
} |
|||
|
|||
public static readonly WeakEvent<EventSource, EventArgs> WeakEv = WeakEvent.Register<EventSource>( |
|||
(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.WeakEv.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.WeakEv.Subscribe(source, new Subscriber(func)); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue