Browse Source

Merge pull request #7301 from AvaloniaUI/feature/register-weak-event

Added reflection-free API for weak events
pull/7310/head
Nikita Tsukanov 4 years ago
committed by GitHub
parent
commit
907a18d832
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 21
      src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs
  2. 10
      src/Avalonia.Base/Data/Core/IndexerNodeBase.cs
  3. 20
      src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs
  4. 24
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  5. 12
      src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs
  6. 187
      src/Avalonia.Base/Utilities/WeakEvent.cs
  7. 40
      src/Avalonia.Base/Utilities/WeakEvents.cs
  8. 35
      src/Avalonia.Base/Utilities/WeakObservable.cs
  9. 1
      src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs
  10. 7
      src/Avalonia.Controls/ApiCompatBaseline.txt
  11. 10
      src/Avalonia.Controls/NativeMenuItem.cs
  12. 32
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  13. 15
      src/Avalonia.Controls/TopLevel.cs
  14. 15
      src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs
  15. 3
      src/Avalonia.Dialogs/ApiCompatBaseline.txt
  16. 17
      src/Avalonia.Layout/AttachedLayout.cs
  17. 36
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs
  18. 3
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs
  19. 12
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs
  20. 11
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs
  21. 27
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs
  22. 3
      tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs
  23. 75
      tests/Avalonia.Base.UnitTests/WeakEventTests.cs
  24. 5
      tests/Avalonia.LeakTests/AvaloniaObjectTests.cs
  25. 7
      tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs
  26. 15
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs

21
src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs

@ -59,7 +59,7 @@ namespace Avalonia.Collections
}
private class WeakCollectionChangedObservable : LightweightObservableBase<NotifyCollectionChangedEventArgs>,
IWeakSubscriber<NotifyCollectionChangedEventArgs>
IWeakEventSubscriber<NotifyCollectionChangedEventArgs>
{
private WeakReference<INotifyCollectionChanged> _sourceReference;
@ -68,31 +68,22 @@ namespace Avalonia.Collections
_sourceReference = source;
}
public void OnEvent(object? sender, NotifyCollectionChangedEventArgs e)
public void OnEvent(object? sender,
WeakEvent ev,
NotifyCollectionChangedEventArgs e)
{
PublishNext(e);
}
protected override void Initialize()
{
if (_sourceReference.TryGetTarget(out var instance))
{
WeakSubscriptionManager.Subscribe(
instance,
nameof(instance.CollectionChanged),
this);
}
WeakEvents.CollectionChanged.Subscribe(instance, this);
}
protected override void Deinitialize()
{
if (_sourceReference.TryGetTarget(out var instance))
{
WeakSubscriptionManager.Unsubscribe(
instance,
nameof(instance.CollectionChanged),
this);
}
WeakEvents.CollectionChanged.Unsubscribe(instance, this);
}
}
}

10
src/Avalonia.Base/Data/Core/IndexerNodeBase.cs

@ -23,18 +23,16 @@ namespace Avalonia.Data.Core
if (incc != null)
{
inputs.Add(WeakObservable.FromEventPattern<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>(
incc,
nameof(incc.CollectionChanged))
inputs.Add(WeakObservable.FromEventPattern(
incc, WeakEvents.CollectionChanged)
.Where(x => ShouldUpdate(x.Sender, x.EventArgs))
.Select(_ => GetValue(target)));
}
if (inpc != null)
{
inputs.Add(WeakObservable.FromEventPattern<INotifyPropertyChanged, PropertyChangedEventArgs>(
inpc,
nameof(inpc.PropertyChanged))
inputs.Add(WeakObservable.FromEventPattern(
inpc, WeakEvents.PropertyChanged)
.Where(x => ShouldUpdate(x.Sender, x.EventArgs))
.Select(_ => GetValue(target)));
}

20
src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs

@ -11,6 +11,12 @@ namespace Avalonia.Data.Core.Plugins
/// </summary>
public class IndeiValidationPlugin : IDataValidationPlugin
{
private static readonly WeakEvent<INotifyDataErrorInfo, DataErrorsChangedEventArgs>
ErrorsChangedWeakEvent = WeakEvent.Register<INotifyDataErrorInfo, DataErrorsChangedEventArgs>(
(s, h) => s.ErrorsChanged += h,
(s, h) => s.ErrorsChanged -= h
);
/// <inheritdoc/>
public bool Match(WeakReference<object?> reference, string memberName)
{
@ -25,7 +31,7 @@ namespace Avalonia.Data.Core.Plugins
return new Validator(reference, name, accessor);
}
private class Validator : DataValidationBase, IWeakSubscriber<DataErrorsChangedEventArgs>
private class Validator : DataValidationBase, IWeakEventSubscriber<DataErrorsChangedEventArgs>
{
private readonly WeakReference<object?> _reference;
private readonly string _name;
@ -37,7 +43,7 @@ namespace Avalonia.Data.Core.Plugins
_name = name;
}
void IWeakSubscriber<DataErrorsChangedEventArgs>.OnEvent(object? sender, DataErrorsChangedEventArgs e)
void IWeakEventSubscriber<DataErrorsChangedEventArgs>.OnEvent(object? notifyDataErrorInfo, WeakEvent ev, DataErrorsChangedEventArgs e)
{
if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName))
{
@ -51,10 +57,7 @@ namespace Avalonia.Data.Core.Plugins
if (target != null)
{
WeakSubscriptionManager.Subscribe(
target,
nameof(target.ErrorsChanged),
this);
ErrorsChangedWeakEvent.Subscribe(target, this);
}
base.SubscribeCore();
@ -66,10 +69,7 @@ namespace Avalonia.Data.Core.Plugins
if (target != null)
{
WeakSubscriptionManager.Unsubscribe(
target,
nameof(target.ErrorsChanged),
this);
ErrorsChangedWeakEvent.Unsubscribe(target, this);
}
base.UnsubscribeCore();

24
src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Reflection;
using Avalonia.Utilities;
@ -85,7 +86,7 @@ namespace Avalonia.Data.Core.Plugins
return found;
}
private class Accessor : PropertyAccessorBase, IWeakSubscriber<PropertyChangedEventArgs>
private class Accessor : PropertyAccessorBase, IWeakEventSubscriber<PropertyChangedEventArgs>
{
private readonly WeakReference<object?> _reference;
private readonly PropertyInfo _property;
@ -129,7 +130,8 @@ namespace Avalonia.Data.Core.Plugins
return false;
}
void IWeakSubscriber<PropertyChangedEventArgs>.OnEvent(object? sender, PropertyChangedEventArgs e)
void IWeakEventSubscriber<PropertyChangedEventArgs>.
OnEvent(object? notifyPropertyChanged, WeakEvent ev, PropertyChangedEventArgs e)
{
if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName))
{
@ -148,13 +150,8 @@ namespace Avalonia.Data.Core.Plugins
{
var inpc = GetReferenceTarget() as INotifyPropertyChanged;
if (inpc != null)
{
WeakSubscriptionManager.Unsubscribe(
inpc,
nameof(inpc.PropertyChanged),
this);
}
if (inpc != null)
WeakEvents.PropertyChanged.Unsubscribe(inpc, this);
}
private object? GetReferenceTarget()
@ -178,13 +175,8 @@ namespace Avalonia.Data.Core.Plugins
{
var inpc = GetReferenceTarget() as INotifyPropertyChanged;
if (inpc != null)
{
WeakSubscriptionManager.Subscribe(
inpc,
nameof(inpc.PropertyChanged),
this);
}
if (inpc != null)
WeakEvents.PropertyChanged.Subscribe(inpc, this);
}
}
}

12
src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs

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

187
src/Avalonia.Base/Utilities/WeakEvent.cs

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

40
src/Avalonia.Base/Utilities/WeakEvents.cs

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

35
src/Avalonia.Base/Utilities/WeakObservable.cs

@ -18,6 +18,7 @@ namespace Avalonia.Utilities
/// <param name="target">Object instance that exposes the event to convert.</param>
/// <param name="eventName">Name of the event to convert.</param>
/// <returns></returns>
[Obsolete("Use WeakEvent-based overload")]
public static IObservable<EventPattern<object, TEventArgs>> FromEventPattern<TTarget, TEventArgs>(
TTarget target,
string eventName)
@ -34,7 +35,9 @@ namespace Avalonia.Utilities
}).Publish().RefCount();
}
private class Handler<TEventArgs> : IWeakSubscriber<TEventArgs> where TEventArgs : EventArgs
private class Handler<TEventArgs>
: IWeakSubscriber<TEventArgs>,
IWeakEventSubscriber<TEventArgs> where TEventArgs : EventArgs
{
private IObserver<EventPattern<object, TEventArgs>> _observer;
@ -47,6 +50,36 @@ namespace Avalonia.Utilities
{
_observer.OnNext(new EventPattern<object, TEventArgs>(sender, e));
}
public void OnEvent(object? sender, WeakEvent ev, TEventArgs e)
{
_observer.OnNext(new EventPattern<object, TEventArgs>(sender, e));
}
}
/// <summary>
/// Converts a WeakEvent conforming to the standard .NET event pattern into an observable
/// sequence, subscribing weakly.
/// </summary>
/// <typeparam name="TTarget">The type of target.</typeparam>
/// <typeparam name="TEventArgs">The type of the event args.</typeparam>
/// <param name="target">Object instance that exposes the event to convert.</param>
/// <param name="ev">The weak event to convert.</param>
/// <returns></returns>
public static IObservable<EventPattern<object, TEventArgs>> FromEventPattern<TTarget, TEventArgs>(
TTarget target, WeakEvent<TTarget, TEventArgs> ev)
where TEventArgs : EventArgs where TTarget : class
{
_ = target ?? throw new ArgumentNullException(nameof(target));
_ = ev ?? throw new ArgumentNullException(nameof(ev));
return Observable.Create<EventPattern<object, TEventArgs>>(observer =>
{
var handler = new Handler<TEventArgs>(observer);
ev.Subscribe(target, handler);
return () => ev.Unsubscribe(target, handler);
}).Publish().RefCount();
}
}
}

1
src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs

@ -19,6 +19,7 @@ namespace Avalonia.Utilities
/// <param name="target">The event source.</param>
/// <param name="eventName">The name of the event.</param>
/// <param name="subscriber">The subscriber.</param>
[Obsolete("Use WeakEvent")]
public static void Subscribe<TTarget, TEventArgs>(TTarget target, string eventName, IWeakSubscriber<TEventArgs> subscriber)
where TEventArgs : EventArgs
{

7
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -29,15 +29,20 @@ MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDownValueChang
MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.NewValue.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.OldValue.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.StyledProperty<System.Boolean> Avalonia.StyledProperty<System.Boolean> Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract.
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.TopLevel' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
MembersMustExist : Member 'public Avalonia.AvaloniaProperty<Avalonia.Media.Stretch> Avalonia.AvaloniaProperty<Avalonia.Media.Stretch> Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract.
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Window' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.WindowBase' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs> Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.ShutdownRequested' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.add_ShutdownRequested(System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs>)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.remove_ShutdownRequested(System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs>)' is present in the implementation but not in the contract.
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Embedding.EmbeddableControlRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
MembersMustExist : Member 'public System.Action<Avalonia.Size> Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action<Avalonia.Size>)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.Platform.ITopLevelNativeMenuExporter.SetNativeMenu(Avalonia.Controls.NativeMenu)' is present in the contract but not in the implementation.
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Primitives.PopupRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract.
@ -57,4 +62,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor
MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract.
Total Issues: 58
Total Issues: 63

10
src/Avalonia.Controls/NativeMenuItem.cs

@ -33,7 +33,7 @@ namespace Avalonia.Controls
}
class CanExecuteChangedSubscriber : IWeakSubscriber<EventArgs>
class CanExecuteChangedSubscriber : IWeakEventSubscriber<EventArgs>
{
private readonly NativeMenuItem _parent;
@ -42,7 +42,7 @@ namespace Avalonia.Controls
_parent = parent;
}
public void OnEvent(object sender, EventArgs e)
public void OnEvent(object? sender, WeakEvent ev, EventArgs e)
{
_parent.CanExecuteChanged();
}
@ -160,14 +160,12 @@ namespace Avalonia.Controls
set
{
if (_command != null)
WeakSubscriptionManager.Unsubscribe(_command,
nameof(ICommand.CanExecuteChanged), _canExecuteChangedSubscriber);
WeakEvents.CommandCanExecuteChanged.Unsubscribe(_command, _canExecuteChangedSubscriber);
SetAndRaise(CommandProperty, ref _command, value);
if (_command != null)
WeakSubscriptionManager.Subscribe(_command,
nameof(ICommand.CanExecuteChanged), _canExecuteChangedSubscriber);
WeakEvents.CommandCanExecuteChanged.Subscribe(_command, _canExecuteChangedSubscriber);
CanExecuteChanged();
}

32
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@ -20,7 +20,7 @@ namespace Avalonia.Controls
/// Represents a data-driven collection control that incorporates a flexible layout system,
/// custom views, and virtualization.
/// </summary>
public class ItemsRepeater : Panel, IChildIndexProvider
public class ItemsRepeater : Panel, IChildIndexProvider, IWeakEventSubscriber<EventArgs>
{
/// <summary>
/// Defines the <see cref="HorizontalCacheLength"/> property.
@ -723,14 +723,8 @@ namespace Avalonia.Controls
{
oldValue.UninitializeForContext(LayoutContext);
WeakEventHandlerManager.Unsubscribe<EventArgs, ItemsRepeater>(
oldValue,
nameof(AttachedLayout.MeasureInvalidated),
InvalidateMeasureForLayout);
WeakEventHandlerManager.Unsubscribe<EventArgs, ItemsRepeater>(
oldValue,
nameof(AttachedLayout.ArrangeInvalidated),
InvalidateArrangeForLayout);
AttachedLayout.MeasureInvalidatedWeakEvent.Unsubscribe(oldValue, this);
AttachedLayout.ArrangeInvalidatedWeakEvent.Unsubscribe(oldValue, this);
// Walk through all the elements and make sure they are cleared
foreach (var element in Children)
@ -748,14 +742,8 @@ namespace Avalonia.Controls
{
newValue.InitializeForContext(LayoutContext);
WeakEventHandlerManager.Subscribe<AttachedLayout, EventArgs, ItemsRepeater>(
newValue,
nameof(AttachedLayout.MeasureInvalidated),
InvalidateMeasureForLayout);
WeakEventHandlerManager.Subscribe<AttachedLayout, EventArgs, ItemsRepeater>(
newValue,
nameof(AttachedLayout.ArrangeInvalidated),
InvalidateArrangeForLayout);
AttachedLayout.MeasureInvalidatedWeakEvent.Subscribe(newValue, this);
AttachedLayout.ArrangeInvalidatedWeakEvent.Subscribe(newValue, this);
}
bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout;
@ -806,9 +794,13 @@ namespace Avalonia.Controls
_viewportManager.OnBringIntoViewRequested(e);
}
private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateMeasure();
private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateArrange();
void IWeakEventSubscriber<EventArgs>.OnEvent(object? sender, WeakEvent ev, EventArgs e)
{
if(ev == AttachedLayout.ArrangeInvalidatedWeakEvent)
InvalidateArrange();
else if (ev == AttachedLayout.MeasureInvalidatedWeakEvent)
InvalidateMeasure();
}
private VirtualizingLayoutContext GetLayoutContext()
{

15
src/Avalonia.Controls/TopLevel.cs

@ -34,7 +34,7 @@ namespace Avalonia.Controls
IStyleHost,
ILogicalRoot,
ITextInputMethodRoot,
IWeakSubscriber<ResourcesChangedEventArgs>
IWeakEventSubscriber<ResourcesChangedEventArgs>
{
/// <summary>
/// Defines the <see cref="ClientSize"/> property.
@ -74,6 +74,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IBrush> TransparencyBackgroundFallbackProperty =
AvaloniaProperty.Register<TopLevel, IBrush>(nameof(TransparencyBackgroundFallback), Brushes.White);
private static readonly WeakEvent<IResourceHost, ResourcesChangedEventArgs>
ResourcesChangedWeakEvent = WeakEvent.Register<IResourceHost, ResourcesChangedEventArgs>(
(s, h) => s.ResourcesChanged += h,
(s, h) => s.ResourcesChanged -= h
);
private readonly IInputManager _inputManager;
private readonly IAccessKeyHandler _accessKeyHandler;
private readonly IKeyboardNavigationHandler _keyboardNavigationHandler;
@ -178,10 +184,7 @@ namespace Avalonia.Controls
if (((IStyleHost)this).StylingParent is IResourceHost applicationResources)
{
WeakSubscriptionManager.Subscribe(
applicationResources,
nameof(IResourceHost.ResourcesChanged),
this);
ResourcesChangedWeakEvent.Subscribe(applicationResources, this);
}
impl.LostFocus += PlatformImpl_LostFocus;
@ -286,7 +289,7 @@ namespace Avalonia.Controls
/// <inheritdoc/>
IMouseDevice IInputRoot.MouseDevice => PlatformImpl?.MouseDevice;
void IWeakSubscriber<ResourcesChangedEventArgs>.OnEvent(object sender, ResourcesChangedEventArgs e)
void IWeakEventSubscriber<ResourcesChangedEventArgs>.OnEvent(object sender, WeakEvent ev, ResourcesChangedEventArgs e)
{
((ILogical)this).NotifyResourcesChanged(e);
}

15
src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs

@ -83,7 +83,7 @@ namespace Avalonia.Controls.Utils
"Collection listener not registered for this collection/listener combination.");
}
private class Entry : IWeakSubscriber<NotifyCollectionChangedEventArgs>, IDisposable
private class Entry : IWeakEventSubscriber<NotifyCollectionChangedEventArgs>, IDisposable
{
private INotifyCollectionChanged _collection;
@ -91,23 +91,18 @@ namespace Avalonia.Controls.Utils
{
_collection = collection;
Listeners = new List<WeakReference<ICollectionChangedListener>>();
WeakSubscriptionManager.Subscribe(
_collection,
nameof(INotifyCollectionChanged.CollectionChanged),
this);
WeakEvents.CollectionChanged.Subscribe(_collection, this);
}
public List<WeakReference<ICollectionChangedListener>> Listeners { get; }
public void Dispose()
{
WeakSubscriptionManager.Unsubscribe(
_collection,
nameof(INotifyCollectionChanged.CollectionChanged),
this);
WeakEvents.CollectionChanged.Unsubscribe(_collection, this);
}
void IWeakSubscriber<NotifyCollectionChangedEventArgs>.OnEvent(object? sender, NotifyCollectionChangedEventArgs e)
void IWeakEventSubscriber<NotifyCollectionChangedEventArgs>.
OnEvent(object? notifyCollectionChanged, WeakEvent ev, NotifyCollectionChangedEventArgs e)
{
static void Notify(
INotifyCollectionChanged incc,

3
src/Avalonia.Dialogs/ApiCompatBaseline.txt

@ -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

17
src/Avalonia.Layout/AttachedLayout.cs

@ -4,6 +4,7 @@
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using Avalonia.Utilities;
namespace Avalonia.Layout
{
@ -19,10 +20,26 @@ namespace Avalonia.Layout
/// </summary>
public event EventHandler? MeasureInvalidated;
/// <summary>
/// Occurs when the measurement state (layout) has been invalidated.
/// </summary>
public static readonly WeakEvent<AttachedLayout, EventArgs> MeasureInvalidatedWeakEvent =
WeakEvent.Register<AttachedLayout>(
(s, h) => s.MeasureInvalidated += h,
(s, h) => s.MeasureInvalidated -= h);
/// <summary>
/// Occurs when the arrange state (layout) has been invalidated.
/// </summary>
public event EventHandler? ArrangeInvalidated;
/// <summary>
/// Occurs when the arrange state (layout) has been invalidated.
/// </summary>
public static readonly WeakEvent<AttachedLayout, EventArgs> ArrangeInvalidatedWeakEvent =
WeakEvent.Register<AttachedLayout>(
(s, h) => s.ArrangeInvalidated += h,
(s, h) => s.ArrangeInvalidated -= h);
/// <summary>
/// Initializes any per-container state the layout requires when it is attached to an

36
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs

@ -72,7 +72,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
}
}
internal class InpcPropertyAccessor : PropertyAccessorBase
internal class InpcPropertyAccessor : PropertyAccessorBase, IWeakEventSubscriber<PropertyChangedEventArgs>
{
protected readonly WeakReference<object> _reference;
private readonly IPropertyInfo _property;
@ -110,7 +110,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
return false;
}
void OnNotifyPropertyChanged(object sender, PropertyChangedEventArgs e)
public void OnEvent(object sender, WeakEvent ev, PropertyChangedEventArgs e)
{
if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName))
{
@ -128,10 +128,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
{
if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc)
{
WeakEventHandlerManager.Unsubscribe<PropertyChangedEventArgs, InpcPropertyAccessor>(
inpc,
nameof(INotifyPropertyChanged.PropertyChanged),
OnNotifyPropertyChanged);
WeakEvents.PropertyChanged.Unsubscribe(inpc, this);
}
}
@ -148,16 +145,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
private void SubscribeToChanges()
{
if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc)
{
WeakEventHandlerManager.Subscribe<INotifyPropertyChanged, PropertyChangedEventArgs, InpcPropertyAccessor>(
inpc,
nameof(INotifyPropertyChanged.PropertyChanged),
OnNotifyPropertyChanged);
}
WeakEvents.PropertyChanged.Subscribe(inpc, this);
}
}
internal class IndexerAccessor : InpcPropertyAccessor
internal class IndexerAccessor : InpcPropertyAccessor, IWeakEventSubscriber<NotifyCollectionChangedEventArgs>
{
private int _index;
@ -172,27 +164,17 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
{
base.SubscribeCore();
if (_reference.TryGetTarget(out var o) && o is INotifyCollectionChanged incc)
{
WeakEventHandlerManager.Subscribe<INotifyCollectionChanged, NotifyCollectionChangedEventArgs, IndexerAccessor>(
incc,
nameof(INotifyCollectionChanged.CollectionChanged),
OnNotifyCollectionChanged);
}
WeakEvents.CollectionChanged.Subscribe(incc, this);
}
protected override void UnsubscribeCore()
{
base.UnsubscribeCore();
if (_reference.TryGetTarget(out var o) && o is INotifyCollectionChanged incc)
{
WeakEventHandlerManager.Unsubscribe<NotifyCollectionChangedEventArgs, IndexerAccessor>(
incc,
nameof(INotifyCollectionChanged.CollectionChanged),
OnNotifyCollectionChanged);
}
WeakEvents.CollectionChanged.Unsubscribe(incc, this);
}
void OnNotifyCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
public void OnEvent(object? sender, WeakEvent ev, NotifyCollectionChangedEventArgs args)
{
if (ShouldNotifyListeners(args))
{

3
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs

@ -6,6 +6,7 @@ using System.Reactive.Linq;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Xunit;
@ -67,6 +68,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
Assert.Equal(1, data.ErrorsChangedSubscriptionCount);
sub.Dispose();
// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();
Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
}

12
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs

@ -9,6 +9,7 @@ using Avalonia.Data.Core;
using Avalonia.UnitTests;
using Xunit;
using Avalonia.Markup.Parsers;
using Avalonia.Threading;
namespace Avalonia.Base.UnitTests.Data.Core
{
@ -110,6 +111,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
data.Foo.Add("baz");
}
// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();
Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "baz" }, result);
Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers());
@ -127,6 +131,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
data.Foo.RemoveAt(0);
}
// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();
Assert.Equal(new[] { "foo", "bar" }, result);
Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers());
@ -145,6 +151,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
data.Foo[1] = "baz";
}
// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();
Assert.Equal(new[] { "bar", "baz" }, result);
Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers());
@ -202,6 +211,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
data.Foo["foo"] = "bar2";
}
// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();
var expected = new[] { "bar", "bar2" };
Assert.Equal(expected, result);
Assert.Equal(0, data.Foo.PropertyChangedSubscriptionCount);

11
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs

@ -5,6 +5,7 @@ using System.Reactive.Subjects;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Xunit;
@ -68,6 +69,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
Assert.Equal(new[] { "foo" }, result);
sub.Dispose();
// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
GC.KeepAlive(data);
@ -109,10 +112,16 @@ namespace Avalonia.Base.UnitTests.Data.Core
var sub = target.Subscribe(x => result.Add(x));
data1.Next.OnNext(data2);
sync.ExecutePostedCallbacks();
// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();
Assert.Equal(new[] { new BindingNotification("foo") }, result);
sub.Dispose();
// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();
Assert.Equal(0, data1.PropertyChangedSubscriptionCount);
GC.KeepAlive(data1);

27
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs

@ -10,6 +10,7 @@ using Avalonia.UnitTests;
using Xunit;
using System.Threading.Tasks;
using Avalonia.Markup.Parsers;
using Avalonia.Threading;
namespace Avalonia.Base.UnitTests.Data.Core
{
@ -182,6 +183,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
sub.Dispose();
// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
GC.KeepAlive(data);
@ -209,8 +213,11 @@ namespace Avalonia.Base.UnitTests.Data.Core
data.RaisePropertyChanged(null);
Assert.Equal(new[] { "foo", "bar", "bar" }, result);
sub.Dispose();
// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
@ -231,7 +238,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
Assert.Equal(new[] { "bar", "baz", null }, result);
sub.Dispose();
// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
@ -253,6 +262,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
Assert.Equal(new[] { "bar", "baz", null }, result);
sub.Dispose();
// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
@ -297,6 +309,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
sub.Dispose();
// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
Assert.Equal(0, old.PropertyChangedSubscriptionCount);
@ -329,6 +344,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
result);
sub.Dispose();
// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
@ -412,6 +430,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
sub1.Dispose();
sub2.Dispose();
// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
GC.KeepAlive(data);
@ -535,6 +556,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
},
result);
// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();
Assert.Equal(0, first.PropertyChangedSubscriptionCount);
Assert.Equal(0, second.PropertyChangedSubscriptionCount);

3
tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs

@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic;
using Avalonia.Data;
using Avalonia.Data.Core.Plugins;
using Avalonia.Threading;
using Xunit;
namespace Avalonia.Base.UnitTests.Data.Core.Plugins
@ -57,6 +58,8 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins
validator.Subscribe(_ => { });
Assert.Equal(1, data.ErrorsChangedSubscriptionCount);
validator.Unsubscribe();
// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();
Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
}

75
tests/Avalonia.Base.UnitTests/WeakEventTests.cs

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

5
tests/Avalonia.LeakTests/AvaloniaObjectTests.cs

@ -1,5 +1,6 @@
using System;
using System.Reactive.Subjects;
using Avalonia.Threading;
using JetBrains.dotMemoryUnit;
using Xunit;
using Xunit.Abstractions;
@ -56,7 +57,9 @@ namespace Avalonia.LeakTests
completeSource();
GC.Collect();
// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();
GC.Collect();
Assert.False(weakSource.IsAlive);
}

7
tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs

@ -12,6 +12,7 @@ using System.Runtime.CompilerServices;
using Avalonia.UnitTests;
using Avalonia.Data.Converters;
using Avalonia.Data.Core;
using Avalonia.Threading;
namespace Avalonia.Markup.UnitTests.Data
{
@ -160,6 +161,9 @@ namespace Avalonia.Markup.UnitTests.Data
target.Bind(TextBlock.TextProperty, new Binding("Foo", BindingMode.OneTime));
target.DataContext = source;
// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();
Assert.Equal(0, source.SubscriberCount);
}
@ -608,6 +612,9 @@ namespace Avalonia.Markup.UnitTests.Data
root.DataContext = source;
}
// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();
Assert.Equal(0, source.SubscriberCount);
}

15
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs

@ -10,6 +10,7 @@ using System.Collections.ObjectModel;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Threading;
using Xunit;
namespace Avalonia.Markup.UnitTests.Parsers
@ -159,7 +160,10 @@ namespace Avalonia.Markup.UnitTests.Parsers
{
data.Foo.Add("baz");
}
// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();
Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "baz" }, result);
Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers());
@ -178,6 +182,9 @@ namespace Avalonia.Markup.UnitTests.Parsers
data.Foo.RemoveAt(0);
}
// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();
Assert.Equal(new[] { "foo", "bar" }, result);
Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers());
@ -196,6 +203,9 @@ namespace Avalonia.Markup.UnitTests.Parsers
data.Foo[1] = "baz";
}
// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();
Assert.Equal(new[] { "bar", "baz" }, result);
Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers());
@ -252,6 +262,9 @@ namespace Avalonia.Markup.UnitTests.Parsers
data.Foo["foo"] = "bar2";
}
// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();
var expected = new[] { "bar", "bar2" };
Assert.Equal(expected, result);
Assert.Equal(0, data.Foo.PropertyChangedSubscriptionCount);

Loading…
Cancel
Save