Browse Source

Started implementing weak bindings.

Tests currently failing as WeakSubscriptionManager doesn't support
unsubscribing yet.
pull/467/head
Steven Kirk 11 years ago
parent
commit
3d271cd020
  1. 39
      src/Markup/Perspex.Markup/Data/ExpressionNode.cs
  2. 20
      src/Markup/Perspex.Markup/Data/ExpressionObserver.cs
  3. 10
      src/Markup/Perspex.Markup/Data/IndexerNode.cs
  4. 13
      src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessorPlugin.cs
  5. 52
      src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs
  6. 47
      src/Markup/Perspex.Markup/Data/Plugins/PerspexPropertyAccessorPlugin.cs
  7. 24
      src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs
  8. 36
      tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs
  9. 1
      tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs
  10. 18
      tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_PerspexProperty.cs
  11. 18
      tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs

39
src/Markup/Perspex.Markup/Data/ExpressionNode.cs

@ -8,46 +8,55 @@ namespace Perspex.Markup.Data
{ {
internal abstract class ExpressionNode : IObservable<object> internal abstract class ExpressionNode : IObservable<object>
{ {
private object _target; protected static readonly WeakReference UnsetReference =
new WeakReference(PerspexProperty.UnsetValue);
private WeakReference _target;
private Subject<object> _subject; private Subject<object> _subject;
private object _value = PerspexProperty.UnsetValue; private WeakReference _value = UnsetReference;
public ExpressionNode Next { get; set; } public ExpressionNode Next { get; set; }
public object Target public WeakReference Target
{ {
get { return _target; } get
{
return _target;
}
set set
{ {
if (!object.Equals(value, _target)) var newInstance = value?.Target;
var oldInstance = _target?.Target;
if (!object.Equals(oldInstance, newInstance))
{ {
if (_target != null) if (oldInstance != null)
{ {
Unsubscribe(_target); Unsubscribe(oldInstance);
} }
_target = value; _target = value;
if (_target != null) if (newInstance != null)
{ {
SubscribeAndUpdate(_target); SubscribeAndUpdate(_target);
} }
else else
{ {
CurrentValue = PerspexProperty.UnsetValue; CurrentValue = UnsetReference;
} }
if (Next != null) if (Next != null)
{ {
Next.Target = CurrentValue; Next.Target = _value;
} }
} }
} }
} }
public object CurrentValue public WeakReference CurrentValue
{ {
get get
{ {
@ -63,7 +72,7 @@ namespace Perspex.Markup.Data
Next.Target = value; Next.Target = value;
} }
_subject?.OnNext(value); _subject?.OnNext(value.Target);
} }
} }
@ -85,14 +94,14 @@ namespace Perspex.Markup.Data
_subject = new Subject<object>(); _subject = new Subject<object>();
} }
observer.OnNext(CurrentValue); observer.OnNext(CurrentValue.Target);
return _subject.Subscribe(observer); return _subject.Subscribe(observer);
} }
} }
protected virtual void SubscribeAndUpdate(object target) protected virtual void SubscribeAndUpdate(WeakReference reference)
{ {
CurrentValue = target; CurrentValue = reference;
} }
protected virtual void Unsubscribe(object target) protected virtual void Unsubscribe(object target)

20
src/Markup/Perspex.Markup/Data/ExpressionObserver.cs

@ -27,7 +27,7 @@ namespace Perspex.Markup.Data
new InpcPropertyAccessorPlugin(), new InpcPropertyAccessorPlugin(),
}; };
private readonly object _root; private readonly WeakReference _root;
private readonly Func<object> _rootGetter; private readonly Func<object> _rootGetter;
private readonly IObservable<object> _rootObservable; private readonly IObservable<object> _rootObservable;
private readonly IObservable<Unit> _update; private readonly IObservable<Unit> _update;
@ -45,7 +45,7 @@ namespace Perspex.Markup.Data
{ {
Contract.Requires<ArgumentNullException>(expression != null); Contract.Requires<ArgumentNullException>(expression != null);
_root = root; _root = new WeakReference(root);
if (!string.IsNullOrWhiteSpace(expression)) if (!string.IsNullOrWhiteSpace(expression))
{ {
@ -115,7 +115,7 @@ namespace Perspex.Markup.Data
if (_rootGetter != null && _node != null) if (_rootGetter != null && _node != null)
{ {
_node.Target = _rootGetter(); _node.Target = new WeakReference(_rootGetter());
} }
try try
@ -155,7 +155,7 @@ namespace Perspex.Markup.Data
} }
else else
{ {
return _root?.GetType(); return _root.Target?.GetType();
} }
} }
finally finally
@ -220,7 +220,9 @@ namespace Perspex.Markup.Data
{ {
if (_update == null) if (_update == null)
{ {
return Observable.Never<object>().StartWith(_root).Subscribe(observer); return Observable.Never<object>()
.StartWith(_root.Target)
.Subscribe(observer);
} }
else else
{ {
@ -238,16 +240,18 @@ namespace Perspex.Markup.Data
{ {
if (_rootGetter != null) if (_rootGetter != null)
{ {
_node.Target = _rootGetter(); _node.Target = new WeakReference(_rootGetter());
if (_update != null) if (_update != null)
{ {
_updateSubscription = _update.Subscribe(x => _node.Target = _rootGetter()); _updateSubscription = _update.Subscribe(x =>
_node.Target = new WeakReference(_rootGetter()));
} }
} }
else if (_rootObservable != null) else if (_rootObservable != null)
{ {
_rootObserverSubscription = _rootObservable.Subscribe(x => _node.Target = x); _rootObserverSubscription = _rootObservable.Subscribe(x =>
_node.Target = new WeakReference(x));
} }
else else
{ {

10
src/Markup/Perspex.Markup/Data/IndexerNode.cs

@ -22,9 +22,11 @@ namespace Perspex.Markup.Data
public IList<string> Arguments { get; } public IList<string> Arguments { get; }
protected override void SubscribeAndUpdate(object target) protected override void SubscribeAndUpdate(WeakReference reference)
{ {
CurrentValue = GetValue(target); object target = reference.Target;
CurrentValue = new WeakReference(GetValue(target));
var incc = target as INotifyCollectionChanged; var incc = target as INotifyCollectionChanged;
@ -50,7 +52,7 @@ namespace Perspex.Markup.Data
} }
if (typeInfo.GetDeclaredProperty(e.PropertyName).GetIndexParameters().Any()) if (typeInfo.GetDeclaredProperty(e.PropertyName).GetIndexParameters().Any())
{ {
CurrentValue = GetValue(sender); CurrentValue = new WeakReference(GetValue(sender));
} }
} }
@ -105,7 +107,7 @@ namespace Perspex.Markup.Data
if (update) if (update)
{ {
CurrentValue = GetValue(sender); CurrentValue = new WeakReference(GetValue(sender));
} }
} }

13
src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessorPlugin.cs

@ -6,7 +6,7 @@ using System;
namespace Perspex.Markup.Data.Plugins namespace Perspex.Markup.Data.Plugins
{ {
/// <summary> /// <summary>
/// Defines how a member is read, written and observed by a /// Defines how a member is read, written and observed by an
/// <see cref="ExpressionObserver"/>. /// <see cref="ExpressionObserver"/>.
/// </summary> /// </summary>
public interface IPropertyAccessorPlugin public interface IPropertyAccessorPlugin
@ -14,20 +14,23 @@ namespace Perspex.Markup.Data.Plugins
/// <summary> /// <summary>
/// Checks whether this plugin can handle accessing the properties of the specified object. /// Checks whether this plugin can handle accessing the properties of the specified object.
/// </summary> /// </summary>
/// <param name="instance">The object.</param> /// <param name="reference">A weak reference to the object.</param>
/// <returns>True if the plugin can handle the object; otherwise false.</returns> /// <returns>True if the plugin can handle the object; otherwise false.</returns>
bool Match(object instance); bool Match(WeakReference reference);
/// <summary> /// <summary>
/// Starts monitoring the value of a property on an object. /// Starts monitoring the value of a property on an object.
/// </summary> /// </summary>
/// <param name="instance">The object.</param> /// <param name="reference">A weak reference to the object.</param>
/// <param name="propertyName">The property name.</param> /// <param name="propertyName">The property name.</param>
/// <param name="changed">A function to call when the property changes.</param> /// <param name="changed">A function to call when the property changes.</param>
/// <returns> /// <returns>
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the /// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made, or null if the property was not found. /// property will be made, or null if the property was not found.
/// </returns> /// </returns>
IPropertyAccessor Start(object instance, string propertyName, Action<object> changed); IPropertyAccessor Start(
WeakReference reference,
string propertyName,
Action<object> changed);
} }
} }

52
src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs

@ -6,6 +6,7 @@ using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reflection; using System.Reflection;
using Perspex.Utilities;
namespace Perspex.Markup.Data.Plugins namespace Perspex.Markup.Data.Plugins
{ {
@ -18,11 +19,11 @@ namespace Perspex.Markup.Data.Plugins
/// <summary> /// <summary>
/// Checks whether this plugin can handle accessing the properties of the specified object. /// Checks whether this plugin can handle accessing the properties of the specified object.
/// </summary> /// </summary>
/// <param name="instance">The object.</param> /// <param name="reference">The object.</param>
/// <returns>True if the plugin can handle the object; otherwise false.</returns> /// <returns>True if the plugin can handle the object; otherwise false.</returns>
public bool Match(object instance) public bool Match(WeakReference reference)
{ {
Contract.Requires<ArgumentNullException>(instance != null); Contract.Requires<ArgumentNullException>(reference != null);
return true; return true;
} }
@ -30,24 +31,28 @@ namespace Perspex.Markup.Data.Plugins
/// <summary> /// <summary>
/// Starts monitoring the value of a property on an object. /// Starts monitoring the value of a property on an object.
/// </summary> /// </summary>
/// <param name="instance">The object.</param> /// <param name="reference">The object.</param>
/// <param name="propertyName">The property name.</param> /// <param name="propertyName">The property name.</param>
/// <param name="changed">A function to call when the property changes.</param> /// <param name="changed">A function to call when the property changes.</param>
/// <returns> /// <returns>
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the /// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made, or null if the property was not found. /// property will be made, or null if the property was not found.
/// </returns> /// </returns>
public IPropertyAccessor Start(object instance, string propertyName, Action<object> changed) public IPropertyAccessor Start(
WeakReference reference,
string propertyName,
Action<object> changed)
{ {
Contract.Requires<ArgumentNullException>(instance != null); Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(propertyName != null); Contract.Requires<ArgumentNullException>(propertyName != null);
Contract.Requires<ArgumentNullException>(changed != null); Contract.Requires<ArgumentNullException>(changed != null);
var instance = reference.Target;
var p = instance.GetType().GetRuntimeProperties().FirstOrDefault(_ => _.Name == propertyName); var p = instance.GetType().GetRuntimeProperties().FirstOrDefault(_ => _.Name == propertyName);
if (p != null) if (p != null)
{ {
return new Accessor(instance, p, changed); return new Accessor(reference, p, changed);
} }
else else
{ {
@ -55,40 +60,49 @@ namespace Perspex.Markup.Data.Plugins
} }
} }
private class Accessor : IPropertyAccessor private class Accessor : IPropertyAccessor, IWeakSubscriber<PropertyChangedEventArgs>
{ {
private readonly object _instance; private readonly WeakReference _reference;
private readonly PropertyInfo _property; private readonly PropertyInfo _property;
private readonly Action<object> _changed; private readonly Action<object> _changed;
public Accessor(object instance, PropertyInfo property, Action<object> changed) public Accessor(
WeakReference reference,
PropertyInfo property,
Action<object> changed)
{ {
Contract.Requires<ArgumentNullException>(instance != null); Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(property != null); Contract.Requires<ArgumentNullException>(property != null);
_instance = instance; _reference = reference;
_property = property; _property = property;
_changed = changed; _changed = changed;
var inpc = instance as INotifyPropertyChanged; var inpc = reference.Target as INotifyPropertyChanged;
if (inpc != null) if (inpc != null)
{ {
inpc.PropertyChanged += PropertyChanged; WeakSubscriptionManager.Subscribe(
inpc,
nameof(inpc.PropertyChanged),
this);
} }
} }
public Type PropertyType => _property.PropertyType; public Type PropertyType => _property.PropertyType;
public object Value => _property.GetValue(_instance); public object Value => _property.GetValue(_reference.Target);
public void Dispose() public void Dispose()
{ {
var inpc = _instance as INotifyPropertyChanged; var inpc = _reference.Target as INotifyPropertyChanged;
if (inpc != null) if (inpc != null)
{ {
inpc.PropertyChanged -= PropertyChanged; //WeakSubscriptionManager.Unsubscribe(
// inpc,
// nameof(inpc.PropertyChanged),
// this);
} }
} }
@ -96,14 +110,14 @@ namespace Perspex.Markup.Data.Plugins
{ {
if (_property.CanWrite) if (_property.CanWrite)
{ {
_property.SetValue(_instance, value); _property.SetValue(_reference.Target, value);
return true; return true;
} }
return false; return false;
} }
private void PropertyChanged(object sender, PropertyChangedEventArgs e) public void OnEvent(object sender, PropertyChangedEventArgs e)
{ {
if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName)) if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName))
{ {

47
src/Markup/Perspex.Markup/Data/Plugins/PerspexPropertyAccessorPlugin.cs

@ -16,37 +16,41 @@ namespace Perspex.Markup.Data.Plugins
/// <summary> /// <summary>
/// Checks whether this plugin can handle accessing the properties of the specified object. /// Checks whether this plugin can handle accessing the properties of the specified object.
/// </summary> /// </summary>
/// <param name="instance">The object.</param> /// <param name="reference">A weak reference to the object.</param>
/// <returns>True if the plugin can handle the object; otherwise false.</returns> /// <returns>True if the plugin can handle the object; otherwise false.</returns>
public bool Match(object instance) public bool Match(WeakReference reference)
{ {
Contract.Requires<ArgumentNullException>(instance != null); Contract.Requires<ArgumentNullException>(reference != null);
return instance is PerspexObject; return reference.Target is PerspexObject;
} }
/// <summary> /// <summary>
/// Starts monitoring the value of a property on an object. /// Starts monitoring the value of a property on an object.
/// </summary> /// </summary>
/// <param name="instance">The object.</param> /// <param name="reference">A weak reference to the object.</param>
/// <param name="propertyName">The property name.</param> /// <param name="propertyName">The property name.</param>
/// <param name="changed">A function to call when the property changes.</param> /// <param name="changed">A function to call when the property changes.</param>
/// <returns> /// <returns>
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the /// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made, or null if the property was not found. /// property will be made, or null if the property was not found.
/// </returns> /// </returns>
public IPropertyAccessor Start(object instance, string propertyName, Action<object> changed) public IPropertyAccessor Start(
WeakReference reference,
string propertyName,
Action<object> changed)
{ {
Contract.Requires<ArgumentNullException>(instance != null); Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(propertyName != null); Contract.Requires<ArgumentNullException>(propertyName != null);
Contract.Requires<ArgumentNullException>(changed != null); Contract.Requires<ArgumentNullException>(changed != null);
var instance = reference.Target;
var o = (PerspexObject)instance; var o = (PerspexObject)instance;
var p = PerspexPropertyRegistry.Instance.FindRegistered(o, propertyName); var p = PerspexPropertyRegistry.Instance.FindRegistered(o, propertyName);
if (p != null) if (p != null)
{ {
return new Accessor(o, p, changed); return new Accessor(new WeakReference<PerspexObject>(o), p, changed);
} }
else else
{ {
@ -56,23 +60,36 @@ namespace Perspex.Markup.Data.Plugins
private class Accessor : IPropertyAccessor private class Accessor : IPropertyAccessor
{ {
private readonly PerspexObject _instance; private readonly WeakReference<PerspexObject> _reference;
private readonly PerspexProperty _property; private readonly PerspexProperty _property;
private IDisposable _subscription; private IDisposable _subscription;
public Accessor(PerspexObject instance, PerspexProperty property, Action<object> changed) public Accessor(
WeakReference<PerspexObject> reference,
PerspexProperty property,
Action<object> changed)
{ {
Contract.Requires<ArgumentNullException>(instance != null); Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(property != null); Contract.Requires<ArgumentNullException>(property != null);
_instance = instance; _reference = reference;
_property = property; _property = property;
_subscription = instance.GetObservable(property).Skip(1).Subscribe(changed); _subscription = Instance.GetObservable(property).Skip(1).Subscribe(changed);
}
public PerspexObject Instance
{
get
{
PerspexObject result;
_reference.TryGetTarget(out result);
return result;
}
} }
public Type PropertyType => _property.PropertyType; public Type PropertyType => _property.PropertyType;
public object Value => _instance.GetValue(_property); public object Value => Instance.GetValue(_property);
public void Dispose() public void Dispose()
{ {
@ -84,7 +101,7 @@ namespace Perspex.Markup.Data.Plugins
{ {
if (!_property.IsReadOnly) if (!_property.IsReadOnly)
{ {
_instance.SetValue(_property, value); Instance.SetValue(_property, value);
return true; return true;
} }

24
src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs

@ -43,15 +43,17 @@ namespace Perspex.Markup.Data
} }
} }
protected override void SubscribeAndUpdate(object target) protected override void SubscribeAndUpdate(WeakReference reference)
{ {
if (target != null) var instance = reference.Target;
if (instance != null)
{ {
var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(target)); var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference));
if (plugin != null) if (plugin != null)
{ {
_accessor = plugin.Start(target, PropertyName, SetCurrentValue); _accessor = plugin.Start(reference, PropertyName, SetCurrentValue);
if (_accessor != null) if (_accessor != null)
{ {
@ -61,7 +63,7 @@ namespace Perspex.Markup.Data
} }
} }
CurrentValue = PerspexProperty.UnsetValue; CurrentValue = UnsetReference;
} }
protected override void Unsubscribe(object target) protected override void Unsubscribe(object target)
@ -81,11 +83,11 @@ namespace Perspex.Markup.Data
// We may need to make this a more general solution. // We may need to make this a more general solution.
if (observable != null && command == null) if (observable != null && command == null)
{ {
CurrentValue = PerspexProperty.UnsetValue; CurrentValue = UnsetReference;
set = true; set = true;
_subscription = observable _subscription = observable
.ObserveOn(SynchronizationContext.Current) .ObserveOn(SynchronizationContext.Current)
.Subscribe(x => CurrentValue = x); .Subscribe(x => CurrentValue = new WeakReference(x));
} }
else if (task != null) else if (task != null)
{ {
@ -95,13 +97,13 @@ namespace Perspex.Markup.Data
{ {
if (task.Status == TaskStatus.RanToCompletion) if (task.Status == TaskStatus.RanToCompletion)
{ {
CurrentValue = resultProperty.GetValue(task); CurrentValue = new WeakReference(resultProperty.GetValue(task));
set = true; set = true;
} }
else else
{ {
task.ContinueWith( task.ContinueWith(
x => CurrentValue = resultProperty.GetValue(task), x => CurrentValue = new WeakReference(resultProperty.GetValue(task)),
TaskScheduler.FromCurrentSynchronizationContext()) TaskScheduler.FromCurrentSynchronizationContext())
.ConfigureAwait(false); .ConfigureAwait(false);
} }
@ -109,13 +111,13 @@ namespace Perspex.Markup.Data
} }
else else
{ {
CurrentValue = value; CurrentValue = new WeakReference(value);
set = true; set = true;
} }
if (!set) if (!set)
{ {
CurrentValue = PerspexProperty.UnsetValue; CurrentValue = UnsetReference;
} }
} }
} }

36
tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs

@ -174,6 +174,42 @@ namespace Perspex.Markup.UnitTests.Data
Assert.Equal(expected, result); Assert.Equal(expected, result);
} }
[Fact]
public void Should_Not_Keep_Source_Alive_ObservableCollection()
{
Func<Tuple<ExpressionObserver, WeakReference>> run = () =>
{
var source = new { Foo = new ObservableCollection<string> { "foo", "bar" } };
var target = new ExpressionObserver(source, "Foo");
return Tuple.Create(target, new WeakReference(source.Foo));
};
var result = run();
result.Item1.Subscribe(x => { });
GC.Collect();
Assert.Null(result.Item2.Target);
}
[Fact]
public void Should_Not_Keep_Source_Alive_NonIntegerIndexer()
{
Func<Tuple<ExpressionObserver, WeakReference>> run = () =>
{
var source = new NonIntegerIndexer();
var target = new ExpressionObserver(source, "Foo");
return Tuple.Create(target, new WeakReference(source));
};
var result = run();
result.Item1.Subscribe(x => { });
GC.Collect();
Assert.Null(result.Item2.Target);
}
private class NonIntegerIndexer : NotifyingBase private class NonIntegerIndexer : NotifyingBase
{ {
private Dictionary<string, string> storage = new Dictionary<string, string>(); private Dictionary<string, string> storage = new Dictionary<string, string>();

1
tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reactive; using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using Microsoft.Reactive.Testing; using Microsoft.Reactive.Testing;
using Perspex.Markup.Data; using Perspex.Markup.Data;

18
tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_PerspexProperty.cs

@ -41,6 +41,24 @@ namespace Perspex.Markup.UnitTests.Data
sub.Dispose(); sub.Dispose();
} }
[Fact]
public void Should_Not_Keep_Source_Alive()
{
Func<Tuple<ExpressionObserver, WeakReference>> run = () =>
{
var source = new Class1();
var target = new ExpressionObserver(source, "Foo");
return Tuple.Create(target, new WeakReference(source));
};
var result = run();
result.Item1.Subscribe(x => { });
GC.Collect();
Assert.Null(result.Item2.Target);
}
private class Class1 : PerspexObject private class Class1 : PerspexObject
{ {
public static readonly StyledProperty<string> FooProperty = public static readonly StyledProperty<string> FooProperty =

18
tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs

@ -331,6 +331,24 @@ namespace Perspex.Markup.UnitTests.Data
Assert.Equal(0, second.SubscriptionCount); Assert.Equal(0, second.SubscriptionCount);
} }
[Fact]
public void Should_Not_Keep_Source_Alive()
{
Func<Tuple<ExpressionObserver, WeakReference>> run = () =>
{
var source = new Class1 { Foo = "foo" };
var target = new ExpressionObserver(source, "Foo");
return Tuple.Create(target, new WeakReference(source));
};
var result = run();
result.Item1.Subscribe(x => { });
GC.Collect();
Assert.Null(result.Item2.Target);
}
private interface INext private interface INext
{ {
int SubscriptionCount { get; } int SubscriptionCount { get; }

Loading…
Cancel
Save