Browse Source

Merge branch 'master' into feature/ApplicationExitMode

pull/1662/head
Steven Kirk 8 years ago
committed by GitHub
parent
commit
ceef449a80
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      .gitignore
  2. 5
      .ncrunch/Avalonia.Designer.HostApp.NetFX.v3.ncrunchproject
  3. 5
      .ncrunch/BindingDemo.net461.v3.ncrunchproject
  4. 5
      .ncrunch/BindingDemo.netcoreapp2.0.v3.ncrunchproject
  5. 5
      .ncrunch/Previewer.v3.ncrunchproject
  6. 5
      .ncrunch/RemoteDemo.v3.ncrunchproject
  7. 5
      .ncrunch/RenderDemo.net461.v3.ncrunchproject
  8. 5
      .ncrunch/RenderDemo.netcoreapp2.0.v3.ncrunchproject
  9. 5
      .ncrunch/VirtualizationDemo.net461.v3.ncrunchproject
  10. 5
      .ncrunch/VirtualizationDemo.netcoreapp2.0.v3.ncrunchproject
  11. 5
      build/System.Memory.props
  12. 2
      packages.cake
  13. 53
      src/Avalonia.Base/AvaloniaObject.cs
  14. 81
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  15. 59
      src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs
  16. 45
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  17. 5
      src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs
  18. 120
      src/Avalonia.Base/Data/Core/ExpressionNode.cs
  19. 88
      src/Avalonia.Base/Data/Core/ExpressionObserver.cs
  20. 11
      src/Avalonia.Base/Data/Core/IndexerNode.cs
  21. 10
      src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs
  22. 10
      src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs
  23. 5
      src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs
  24. 14
      src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs
  25. 19
      src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs
  26. 16
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  27. 8
      src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs
  28. 68
      src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs
  29. 11
      src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs
  30. 29
      src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
  31. 17
      src/Avalonia.Base/Data/Core/StreamNode.cs
  32. 42
      src/Avalonia.Base/Reactive/AvaloniaObservable.cs
  33. 46
      src/Avalonia.Base/Reactive/AvaloniaPropertyChangedObservable.cs
  34. 52
      src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs
  35. 202
      src/Avalonia.Base/Reactive/LightweightObservableBase.cs
  36. 76
      src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs
  37. 85
      src/Avalonia.Base/Reactive/WeakPropertyChangedObservable.cs
  38. 1
      src/Avalonia.Controls/ItemsControl.cs
  39. 40
      src/Avalonia.Controls/Mixins/ContentControlMixin.cs
  40. 11
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  41. 1
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  42. 39
      src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs
  43. 162
      src/Avalonia.Styling/LogicalTree/ControlLocator.cs
  44. 66
      src/Avalonia.Styling/Styling/ActivatedObservable.cs
  45. 71
      src/Avalonia.Styling/Styling/ActivatedSubject.cs
  46. 111
      src/Avalonia.Styling/Styling/ActivatedValue.cs
  47. 4
      src/Avalonia.Styling/Styling/StyleActivator.cs
  48. 68
      src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs
  49. 1
      src/Avalonia.Visuals/Avalonia.Visuals.csproj
  50. 670
      src/Avalonia.Visuals/Media/PathMarkupParser.cs
  51. 67
      src/Avalonia.Visuals/VisualTree/VisualLocator.cs
  52. 44
      src/Markup/Avalonia.Markup/Data/Binding.cs
  53. 2
      src/OSX/Avalonia.MonoMac/KeyTransform.cs
  54. 3
      src/Windows/Avalonia.Win32/ClipboardImpl.cs
  55. 15
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
  56. 5
      tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs
  57. 20
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  58. 18
      tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs
  59. 7
      tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs
  60. 10
      tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs
  61. 14
      tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs
  62. 24
      tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs
  63. 28
      tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs

8
.gitignore

@ -176,5 +176,9 @@ nuget
Avalonia.XBuild.sln Avalonia.XBuild.sln
project.lock.json project.lock.json
.idea/* .idea/*
**/obj-Skia/*
**/obj-Direct2D1/*
##################
## BenchmarkDotNet
##################
BenchmarkDotNet.Artifacts/

5
.ncrunch/Avalonia.Designer.HostApp.NetFX.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/BindingDemo.net461.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/BindingDemo.netcoreapp2.0.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/Previewer.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/RemoteDemo.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/RenderDemo.net461.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/RenderDemo.netcoreapp2.0.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/VirtualizationDemo.net461.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/VirtualizationDemo.netcoreapp2.0.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
build/System.Memory.props

@ -0,0 +1,5 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="System.Memory" Version="4.5.0" />
</ItemGroup>
</Project>

2
packages.cake

@ -253,7 +253,7 @@ public class Packages
} }
.Deps(new string[]{null, "netcoreapp2.0"}, .Deps(new string[]{null, "netcoreapp2.0"},
"System.ValueTuple", "System.ComponentModel.TypeConverter", "System.ComponentModel.Primitives", "System.ValueTuple", "System.ComponentModel.TypeConverter", "System.ComponentModel.Primitives",
"System.Runtime.Serialization.Primitives", "System.Xml.XmlDocument", "System.Xml.ReaderWriter") "System.Runtime.Serialization.Primitives", "System.Xml.XmlDocument", "System.Xml.ReaderWriter", "System.Memory")
.ToArray(), .ToArray(),
Files = coreLibrariesNuSpecContent Files = coreLibrariesNuSpecContent
.Concat(win32CoreLibrariesNuSpecContent).Concat(net45RuntimePlatform) .Concat(win32CoreLibrariesNuSpecContent).Concat(net45RuntimePlatform)

53
src/Avalonia.Base/AvaloniaObject.cs

@ -10,6 +10,7 @@ using System.Reactive.Linq;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Diagnostics; using Avalonia.Diagnostics;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Reactive;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -38,7 +39,7 @@ namespace Avalonia
/// Maintains a list of direct property binding subscriptions so that the binding source /// Maintains a list of direct property binding subscriptions so that the binding source
/// doesn't get collected. /// doesn't get collected.
/// </summary> /// </summary>
private List<IDisposable> _directBindings; private List<DirectBindingSubscription> _directBindings;
/// <summary> /// <summary>
/// Event handler for <see cref="INotifyPropertyChanged"/> implementation. /// Event handler for <see cref="INotifyPropertyChanged"/> implementation.
@ -359,25 +360,12 @@ namespace Avalonia
property, property,
description); description);
IDisposable subscription = null;
if (_directBindings == null) if (_directBindings == null)
{ {
_directBindings = new List<IDisposable>(); _directBindings = new List<DirectBindingSubscription>();
} }
subscription = source return new DirectBindingSubscription(this, property, source);
.Select(x => CastOrDefault(x, property.PropertyType))
.Do(_ => { }, () => _directBindings.Remove(subscription))
.Subscribe(x => SetDirectValue(property, x));
_directBindings.Add(subscription);
return Disposable.Create(() =>
{
subscription.Dispose();
_directBindings.Remove(subscription);
});
} }
else else
{ {
@ -908,5 +896,38 @@ namespace Avalonia
value, value,
priority); priority);
} }
private class DirectBindingSubscription : IObserver<object>, IDisposable
{
readonly AvaloniaObject _owner;
readonly AvaloniaProperty _property;
IDisposable _subscription;
public DirectBindingSubscription(
AvaloniaObject owner,
AvaloniaProperty property,
IObservable<object> source)
{
_owner = owner;
_property = property;
_owner._directBindings.Add(this);
_subscription = source.Subscribe(this);
}
public void Dispose()
{
_subscription.Dispose();
_owner._directBindings.Remove(this);
}
public void OnCompleted() => Dispose();
public void OnError(Exception error) => Dispose();
public void OnNext(object value)
{
var castValue = CastOrDefault(value, _property.PropertyType);
_owner.SetDirectValue(_property, castValue);
}
}
} }
} }

81
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -36,32 +36,15 @@ namespace Avalonia
/// An observable which fires immediately with the current value of the property on the /// An observable which fires immediately with the current value of the property on the
/// object and subsequently each time the property value changes. /// object and subsequently each time the property value changes.
/// </returns> /// </returns>
/// <remarks>
/// The subscription to <paramref name="o"/> is created using a weak reference.
/// </remarks>
public static IObservable<object> GetObservable(this IAvaloniaObject o, AvaloniaProperty property) public static IObservable<object> GetObservable(this IAvaloniaObject o, AvaloniaProperty property)
{ {
Contract.Requires<ArgumentNullException>(o != null); Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(property != null); Contract.Requires<ArgumentNullException>(property != null);
return new AvaloniaObservable<object>( return new AvaloniaPropertyObservable<object>(o, property);
observer =>
{
EventHandler<AvaloniaPropertyChangedEventArgs> handler = (s, e) =>
{
if (e.Property == property)
{
observer.OnNext(e.NewValue);
}
};
observer.OnNext(o.GetValue(property));
o.PropertyChanged += handler;
return Disposable.Create(() =>
{
o.PropertyChanged -= handler;
});
},
GetDescription(o, property));
} }
/// <summary> /// <summary>
@ -74,51 +57,36 @@ namespace Avalonia
/// An observable which fires immediately with the current value of the property on the /// An observable which fires immediately with the current value of the property on the
/// object and subsequently each time the property value changes. /// object and subsequently each time the property value changes.
/// </returns> /// </returns>
/// <remarks>
/// The subscription to <paramref name="o"/> is created using a weak reference.
/// </remarks>
public static IObservable<T> GetObservable<T>(this IAvaloniaObject o, AvaloniaProperty<T> property) public static IObservable<T> GetObservable<T>(this IAvaloniaObject o, AvaloniaProperty<T> property)
{ {
Contract.Requires<ArgumentNullException>(o != null); Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(property != null); Contract.Requires<ArgumentNullException>(property != null);
return o.GetObservable((AvaloniaProperty)property).Cast<T>(); return new AvaloniaPropertyObservable<T>(o, property);
} }
/// <summary> /// <summary>
/// Gets an observable for a <see cref="AvaloniaProperty"/>. /// Gets an observable that listens for property changed events for an
/// <see cref="AvaloniaProperty"/>.
/// </summary> /// </summary>
/// <param name="o">The object.</param> /// <param name="o">The object.</param>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <returns> /// <returns>
/// An observable which when subscribed pushes the old and new values of the property each /// An observable which when subscribed pushes the property changed event args
/// time it is changed. Note that the observable returned from this method does not fire /// each time a <see cref="IAvaloniaObject.PropertyChanged"/> event is raised
/// with the current value of the property immediately. /// for the specified property.
/// </returns> /// </returns>
public static IObservable<Tuple<T, T>> GetObservableWithHistory<T>( public static IObservable<AvaloniaPropertyChangedEventArgs> GetPropertyChangedObservable(
this IAvaloniaObject o, this IAvaloniaObject o,
AvaloniaProperty<T> property) AvaloniaProperty property)
{ {
Contract.Requires<ArgumentNullException>(o != null); Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(property != null); Contract.Requires<ArgumentNullException>(property != null);
return new AvaloniaObservable<Tuple<T, T>>( return new AvaloniaPropertyChangedObservable(o, property);
observer =>
{
EventHandler<AvaloniaPropertyChangedEventArgs> handler = (s, e) =>
{
if (e.Property == property)
{
observer.OnNext(Tuple.Create((T)e.OldValue, (T)e.NewValue));
}
};
o.PropertyChanged += handler;
return Disposable.Create(() =>
{
o.PropertyChanged -= handler;
});
},
GetDescription(o, property));
} }
/// <summary> /// <summary>
@ -166,23 +134,6 @@ namespace Avalonia
o.GetObservable(property)); o.GetObservable(property));
} }
/// <summary>
/// Gets a weak observable for a <see cref="AvaloniaProperty"/>.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="property">The property.</param>
/// <returns>An observable.</returns>
public static IObservable<object> GetWeakObservable(this IAvaloniaObject o, AvaloniaProperty property)
{
Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(property != null);
return new WeakPropertyChangedObservable(
new WeakReference<IAvaloniaObject>(o),
property,
GetDescription(o, property));
}
/// <summary> /// <summary>
/// Binds a property on an <see cref="IAvaloniaObject"/> to an <see cref="IBinding"/>. /// Binds a property on an <see cref="IAvaloniaObject"/> to an <see cref="IBinding"/>.
/// </summary> /// </summary>

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

@ -2,13 +2,9 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects; using Avalonia.Reactive;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Collections namespace Avalonia.Collections
@ -43,9 +39,8 @@ namespace Avalonia.Collections
Contract.Requires<ArgumentNullException>(collection != null); Contract.Requires<ArgumentNullException>(collection != null);
Contract.Requires<ArgumentNullException>(handler != null); Contract.Requires<ArgumentNullException>(handler != null);
return return collection.GetWeakCollectionChangedObservable()
collection.GetWeakCollectionChangedObservable() .Subscribe(e => handler(collection, e));
.Subscribe(e => handler.Invoke(collection, e));
} }
/// <summary> /// <summary>
@ -63,18 +58,13 @@ namespace Avalonia.Collections
Contract.Requires<ArgumentNullException>(collection != null); Contract.Requires<ArgumentNullException>(collection != null);
Contract.Requires<ArgumentNullException>(handler != null); Contract.Requires<ArgumentNullException>(handler != null);
return return collection.GetWeakCollectionChangedObservable().Subscribe(handler);
collection.GetWeakCollectionChangedObservable()
.Subscribe(handler);
} }
private class WeakCollectionChangedObservable : ObservableBase<NotifyCollectionChangedEventArgs>, private class WeakCollectionChangedObservable : LightweightObservableBase<NotifyCollectionChangedEventArgs>,
IWeakSubscriber<NotifyCollectionChangedEventArgs> IWeakSubscriber<NotifyCollectionChangedEventArgs>
{ {
private WeakReference<INotifyCollectionChanged> _sourceReference; private WeakReference<INotifyCollectionChanged> _sourceReference;
private readonly Subject<NotifyCollectionChangedEventArgs> _changed = new Subject<NotifyCollectionChangedEventArgs>();
private int _count;
public WeakCollectionChangedObservable(WeakReference<INotifyCollectionChanged> source) public WeakCollectionChangedObservable(WeakReference<INotifyCollectionChanged> source)
{ {
@ -83,43 +73,28 @@ namespace Avalonia.Collections
public void OnEvent(object sender, NotifyCollectionChangedEventArgs e) public void OnEvent(object sender, NotifyCollectionChangedEventArgs e)
{ {
_changed.OnNext(e); PublishNext(e);
} }
protected override IDisposable SubscribeCore(IObserver<NotifyCollectionChangedEventArgs> observer) protected override void Initialize()
{ {
if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance)) if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance))
{ {
if (_count++ == 0) WeakSubscriptionManager.Subscribe(
{ instance,
WeakSubscriptionManager.Subscribe( nameof(instance.CollectionChanged),
instance, this);
nameof(instance.CollectionChanged),
this);
}
return Observable.Using(() => Disposable.Create(DecrementCount), _ => _changed)
.Subscribe(observer);
}
else
{
_changed.OnCompleted();
observer.OnCompleted();
return Disposable.Empty;
} }
} }
private void DecrementCount() protected override void Deinitialize()
{ {
if (--_count == 0) if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance))
{ {
if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance)) WeakSubscriptionManager.Unsubscribe(
{ instance,
WeakSubscriptionManager.Unsubscribe( nameof(instance.CollectionChanged),
instance, this);
nameof(instance.CollectionChanged),
this);
}
} }
} }
} }

45
src/Avalonia.Base/Data/Core/BindingExpression.cs

@ -7,21 +7,23 @@ using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Reactive;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Data.Core namespace Avalonia.Data.Core
{ {
/// <summary> /// <summary>
/// Binds to an expression on an object using a type value converter to convert the values /// Binds to an expression on an object using a type value converter to convert the values
/// that are send and received. /// that are sent and received.
/// </summary> /// </summary>
public class BindingExpression : ISubject<object>, IDescription public class BindingExpression : LightweightObservableBase<object>, ISubject<object>, IDescription
{ {
private readonly ExpressionObserver _inner; private readonly ExpressionObserver _inner;
private readonly Type _targetType; private readonly Type _targetType;
private readonly object _fallbackValue; private readonly object _fallbackValue;
private readonly BindingPriority _priority; private readonly BindingPriority _priority;
private readonly Subject<object> _errors = new Subject<object>(); InnerListener _innerListener;
WeakReference<object> _value;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class. /// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
@ -139,7 +141,7 @@ namespace Avalonia.Data.Core
"IValueConverter should not return non-errored BindingNotification."); "IValueConverter should not return non-errored BindingNotification.");
} }
_errors.OnNext(notification); PublishNext(notification);
if (_fallbackValue != AvaloniaProperty.UnsetValue) if (_fallbackValue != AvaloniaProperty.UnsetValue)
{ {
@ -170,12 +172,18 @@ namespace Avalonia.Data.Core
} }
} }
/// <inheritdoc/> protected override void Initialize() => _innerListener = new InnerListener(this);
public IDisposable Subscribe(IObserver<object> observer) protected override void Deinitialize() => _innerListener.Dispose();
protected override void Subscribed(IObserver<object> observer, bool first)
{ {
return _inner.Select(ConvertValue).Merge(_errors).Subscribe(observer); if (!first && _value != null && _value.TryGetTarget(out var val) == true)
{
observer.OnNext(val);
}
} }
/// <inheritdoc/>
private object ConvertValue(object value) private object ConvertValue(object value)
{ {
var notification = value as BindingNotification; var notification = value as BindingNotification;
@ -301,5 +309,28 @@ namespace Avalonia.Data.Core
return a; return a;
} }
public class InnerListener : IObserver<object>, IDisposable
{
private readonly BindingExpression _owner;
private readonly IDisposable _dispose;
public InnerListener(BindingExpression owner)
{
_owner = owner;
_dispose = owner._inner.Subscribe(this);
}
public void Dispose() => _dispose.Dispose();
public void OnCompleted() => _owner.PublishCompleted();
public void OnError(Exception error) => _owner.PublishError(error);
public void OnNext(object value)
{
var converted = _owner.ConvertValue(value);
_owner._value = new WeakReference<object>(converted);
_owner.PublishNext(converted);
}
}
} }
} }

5
src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs

@ -9,10 +9,5 @@ namespace Avalonia.Data.Core
internal class EmptyExpressionNode : ExpressionNode internal class EmptyExpressionNode : ExpressionNode
{ {
public override string Description => "."; public override string Description => ".";
protected override IObservable<object> StartListeningCore(WeakReference reference)
{
return Observable.Return(reference.Target);
}
} }
} }

120
src/Avalonia.Base/Data/Core/ExpressionNode.cs

@ -2,22 +2,18 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Data;
namespace Avalonia.Data.Core namespace Avalonia.Data.Core
{ {
internal abstract class ExpressionNode : ISubject<object> internal abstract class ExpressionNode
{ {
private static readonly object CacheInvalid = new object(); private static readonly object CacheInvalid = new object();
protected static readonly WeakReference UnsetReference = protected static readonly WeakReference UnsetReference =
new WeakReference(AvaloniaProperty.UnsetValue); new WeakReference(AvaloniaProperty.UnsetValue);
private WeakReference _target = UnsetReference; private WeakReference _target = UnsetReference;
private IDisposable _valueSubscription; private Action<object> _subscriber;
private IObserver<object> _observer; private bool _listening;
protected WeakReference LastValue { get; private set; } protected WeakReference LastValue { get; private set; }
@ -33,92 +29,66 @@ namespace Avalonia.Data.Core
var oldTarget = _target?.Target; var oldTarget = _target?.Target;
var newTarget = value.Target; var newTarget = value.Target;
var running = _valueSubscription != null;
if (!ReferenceEquals(oldTarget, newTarget)) if (!ReferenceEquals(oldTarget, newTarget))
{ {
_valueSubscription?.Dispose(); if (_listening)
_valueSubscription = null; {
StopListening();
}
_target = value; _target = value;
if (running) if (_subscriber != null)
{ {
_valueSubscription = StartListening(); StartListening();
} }
} }
} }
} }
public IDisposable Subscribe(IObserver<object> observer) public void Subscribe(Action<object> subscriber)
{ {
if (_observer != null) if (_subscriber != null)
{ {
throw new AvaloniaInternalException("ExpressionNode can only be subscribed once."); throw new AvaloniaInternalException("ExpressionNode can only be subscribed once.");
} }
_observer = observer; _subscriber = subscriber;
var nextSubscription = Next?.Subscribe(this); Next?.Subscribe(NextValueChanged);
_valueSubscription = StartListening(); StartListening();
return Disposable.Create(() =>
{
_valueSubscription?.Dispose();
_valueSubscription = null;
LastValue = null;
nextSubscription?.Dispose();
_observer = null;
});
} }
void IObserver<object>.OnCompleted() public void Unsubscribe()
{ {
throw new AvaloniaInternalException("ExpressionNode.OnCompleted should not be called."); Next?.Unsubscribe();
}
void IObserver<object>.OnError(Exception error) if (_listening)
{ {
throw new AvaloniaInternalException("ExpressionNode.OnError should not be called."); StopListening();
}
LastValue = null;
_subscriber = null;
} }
void IObserver<object>.OnNext(object value) protected virtual void StartListeningCore(WeakReference reference)
{ {
NextValueChanged(value); ValueChanged(reference.Target);
} }
protected virtual IObservable<object> StartListeningCore(WeakReference reference) protected virtual void StopListeningCore()
{ {
return Observable.Return(reference.Target);
} }
protected virtual void NextValueChanged(object value) protected virtual void NextValueChanged(object value)
{ {
var bindingBroken = BindingNotification.ExtractError(value) as MarkupBindingChainException; var bindingBroken = BindingNotification.ExtractError(value) as MarkupBindingChainException;
bindingBroken?.AddNode(Description); bindingBroken?.AddNode(Description);
_observer.OnNext(value); _subscriber(value);
}
private IDisposable StartListening()
{
var target = _target.Target;
IObservable<object> source;
if (target == null)
{
source = Observable.Return(TargetNullNotification());
}
else if (target == AvaloniaProperty.UnsetValue)
{
source = Observable.Empty<object>();
}
else
{
source = StartListeningCore(_target);
}
return source.Subscribe(ValueChanged);
} }
private void ValueChanged(object value) protected void ValueChanged(object value)
{ {
var notification = value as BindingNotification; var notification = value as BindingNotification;
@ -131,24 +101,50 @@ namespace Avalonia.Data.Core
} }
else else
{ {
_observer.OnNext(value); _subscriber(value);
} }
} }
else else
{ {
LastValue = new WeakReference(notification.Value); LastValue = new WeakReference(notification.Value);
if (Next != null) if (Next != null)
{ {
Next.Target = new WeakReference(notification.Value); Next.Target = new WeakReference(notification.Value);
} }
if (Next == null || notification.Error != null) if (Next == null || notification.Error != null)
{ {
_observer.OnNext(value); _subscriber(value);
} }
} }
} }
private void StartListening()
{
var target = _target.Target;
if (target == null)
{
ValueChanged(TargetNullNotification());
_listening = false;
}
else if (target != AvaloniaProperty.UnsetValue)
{
StartListeningCore(_target);
_listening = true;
}
else
{
_listening = false;
}
}
private void StopListening()
{
StopListeningCore();
}
private BindingNotification TargetNullNotification() private BindingNotification TargetNullNotification()
{ {
return new BindingNotification( return new BindingNotification(

88
src/Avalonia.Base/Data/Core/ExpressionObserver.cs

@ -4,18 +4,17 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reactive; using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core.Plugins; using Avalonia.Data.Core.Plugins;
using Avalonia.Reactive;
namespace Avalonia.Data.Core namespace Avalonia.Data.Core
{ {
/// <summary> /// <summary>
/// Observes and sets the value of an expression on an object. /// Observes and sets the value of an expression on an object.
/// </summary> /// </summary>
public class ExpressionObserver : ObservableBase<object>, IDescription public class ExpressionObserver : LightweightObservableBase<object>, IDescription
{ {
/// <summary> /// <summary>
/// An ordered collection of property accessor plugins that can be used to customize /// An ordered collection of property accessor plugins that can be used to customize
@ -54,9 +53,9 @@ namespace Avalonia.Data.Core
private static readonly object UninitializedValue = new object(); private static readonly object UninitializedValue = new object();
private readonly ExpressionNode _node; private readonly ExpressionNode _node;
private readonly Subject<Unit> _finished; private object _root;
private readonly object _root; private IDisposable _rootSubscription;
private IObservable<object> _result; private WeakReference<object> _value;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class. /// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
@ -107,7 +106,6 @@ namespace Avalonia.Data.Core
Expression = expression; Expression = expression;
Description = description ?? expression; Description = description ?? expression;
_node = Parse(expression, enableDataValidation); _node = Parse(expression, enableDataValidation);
_finished = new Subject<Unit>();
_root = rootObservable; _root = rootObservable;
} }
@ -135,8 +133,6 @@ namespace Avalonia.Data.Core
Expression = expression; Expression = expression;
Description = description ?? expression; Description = description ?? expression;
_node = Parse(expression, enableDataValidation); _node = Parse(expression, enableDataValidation);
_finished = new Subject<Unit>();
_node.Target = new WeakReference(rootGetter()); _node.Target = new WeakReference(rootGetter());
_root = update.Select(x => rootGetter()); _root = update.Select(x => rootGetter());
} }
@ -203,27 +199,26 @@ namespace Avalonia.Data.Core
} }
} }
/// <inheritdoc/> protected override void Initialize()
protected override IDisposable SubscribeCore(IObserver<object> observer)
{ {
if (_result == null) _value = null;
{ _node.Subscribe(ValueChanged);
var source = (IObservable<object>)_node; StartRoot();
}
if (_finished != null) protected override void Deinitialize()
{ {
source = source.TakeUntil(_finished); _rootSubscription?.Dispose();
} _rootSubscription = null;
_node.Unsubscribe();
}
_result = Observable.Using(StartRoot, _ => source) protected override void Subscribed(IObserver<object> observer, bool first)
.Select(ToWeakReference) {
.Publish(UninitializedValue) if (!first && _value != null && _value.TryGetTarget(out var value))
.RefCount() {
.Where(x => x != UninitializedValue) observer.OnNext(value);
.Select(Translate);
} }
return _result.Subscribe(observer);
} }
private static ExpressionNode Parse(string expression, bool enableDataValidation) private static ExpressionNode Parse(string expression, bool enableDataValidation)
@ -238,42 +233,27 @@ namespace Avalonia.Data.Core
} }
} }
private static object ToWeakReference(object o) private void StartRoot()
{ {
return o is BindingNotification ? o : new WeakReference(o); if (_root is IObservable<object> observable)
}
private object Translate(object o)
{
if (o is WeakReference weak)
{ {
return weak.Target; _rootSubscription = observable.Subscribe(
x => _node.Target = new WeakReference(x != AvaloniaProperty.UnsetValue ? x : null),
x => PublishCompleted(),
() => PublishCompleted());
} }
else if (BindingNotification.ExtractError(o) is MarkupBindingChainException broken) else
{ {
broken.Commit(Description); _node.Target = (WeakReference)_root;
} }
return o;
} }
private IDisposable StartRoot() private void ValueChanged(object value)
{ {
switch (_root) var broken = BindingNotification.ExtractError(value) as MarkupBindingChainException;
{ broken?.Commit(Description);
case IObservable<object> observable: _value = new WeakReference<object>(value);
return observable.Subscribe( PublishNext(value);
x => _node.Target = new WeakReference(x != AvaloniaProperty.UnsetValue ? x : null),
_ => _finished.OnNext(Unit.Default),
() => _finished.OnNext(Unit.Default));
case WeakReference weak:
_node.Target = weak;
break;
default:
throw new AvaloniaInternalException("The ExpressionObserver._root member should only be either an observable or WeakReference.");
}
return Disposable.Empty;
} }
} }
} }

11
src/Avalonia.Base/Data/Core/IndexerNode.cs

@ -17,6 +17,8 @@ namespace Avalonia.Data.Core
{ {
internal class IndexerNode : SettableNode internal class IndexerNode : SettableNode
{ {
private IDisposable _subscription;
public IndexerNode(IList<string> arguments) public IndexerNode(IList<string> arguments)
{ {
Arguments = arguments; Arguments = arguments;
@ -24,7 +26,7 @@ namespace Avalonia.Data.Core
public override string Description => "[" + string.Join(",", Arguments) + "]"; public override string Description => "[" + string.Join(",", Arguments) + "]";
protected override IObservable<object> StartListeningCore(WeakReference reference) protected override void StartListeningCore(WeakReference reference)
{ {
var target = reference.Target; var target = reference.Target;
var incc = target as INotifyCollectionChanged; var incc = target as INotifyCollectionChanged;
@ -49,7 +51,12 @@ namespace Avalonia.Data.Core
.Select(_ => GetValue(target))); .Select(_ => GetValue(target)));
} }
return Observable.Merge(inputs).StartWith(GetValue(target)); _subscription = Observable.Merge(inputs).StartWith(GetValue(target)).Subscribe(ValueChanged);
}
protected override void StopListeningCore()
{
_subscription.Dispose();
} }
protected override bool SetTargetValueCore(object value, BindingPriority priority) protected override bool SetTargetValueCore(object value, BindingPriority priority)

10
src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs

@ -145,15 +145,15 @@ namespace Avalonia.Data.Core.Plugins
return false; return false;
} }
protected override void Dispose(bool disposing) protected override void SubscribeCore()
{ {
_subscription?.Dispose(); _subscription = Instance?.GetObservable(_property).Subscribe(PublishValue);
_subscription = null;
} }
protected override void SubscribeCore(IObserver<object> observer) protected override void UnsubscribeCore()
{ {
_subscription = Instance?.GetWeakObservable(_property).Subscribe(observer); _subscription?.Dispose();
_subscription = null;
} }
} }
} }

10
src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs

@ -55,13 +55,13 @@ namespace Avalonia.Data.Core.Plugins
/// <param name="value">The value.</param> /// <param name="value">The value.</param>
void IObserver<object>.OnNext(object value) => InnerValueChanged(value); void IObserver<object>.OnNext(object value) => InnerValueChanged(value);
/// <inheritdoc/>
protected override void Dispose(bool disposing) => _inner.Dispose();
/// <summary> /// <summary>
/// Begins listening to the inner <see cref="IPropertyAccessor"/>. /// Begins listening to the inner <see cref="IPropertyAccessor"/>.
/// </summary> /// </summary>
protected override void SubscribeCore(IObserver<object> observer) => _inner.Subscribe(this); protected override void SubscribeCore() => _inner.Subscribe(InnerValueChanged);
/// <inheritdoc/>
protected override void UnsubscribeCore() => _inner.Dispose();
/// <summary> /// <summary>
/// Called when the inner <see cref="IPropertyAccessor"/> notifies with a new value. /// Called when the inner <see cref="IPropertyAccessor"/> notifies with a new value.
@ -74,7 +74,7 @@ namespace Avalonia.Data.Core.Plugins
protected virtual void InnerValueChanged(object value) protected virtual void InnerValueChanged(object value)
{ {
var notification = value as BindingNotification ?? new BindingNotification(value); var notification = value as BindingNotification ?? new BindingNotification(value);
Observer.OnNext(notification); PublishValue(notification);
} }
} }
} }

5
src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs

@ -1,7 +1,6 @@
// Copyright (c) The Avalonia Project. All rights reserved. // Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Data;
using System; using System;
using System.Reflection; using System.Reflection;
@ -36,11 +35,11 @@ namespace Avalonia.Data.Core.Plugins
} }
catch (TargetInvocationException ex) catch (TargetInvocationException ex)
{ {
Observer.OnNext(new BindingNotification(ex.InnerException, BindingErrorType.DataValidationError)); PublishValue(new BindingNotification(ex.InnerException, BindingErrorType.DataValidationError));
} }
catch (Exception ex) catch (Exception ex)
{ {
Observer.OnNext(new BindingNotification(ex, BindingErrorType.DataValidationError)); PublishValue(new BindingNotification(ex, BindingErrorType.DataValidationError));
} }
return false; return false;

14
src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs

@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using Avalonia.Data;
namespace Avalonia.Data.Core.Plugins namespace Avalonia.Data.Core.Plugins
{ {
@ -10,7 +9,7 @@ namespace Avalonia.Data.Core.Plugins
/// Defines an accessor to a property on an object returned by a /// Defines an accessor to a property on an object returned by a
/// <see cref="IPropertyAccessorPlugin"/> /// <see cref="IPropertyAccessorPlugin"/>
/// </summary> /// </summary>
public interface IPropertyAccessor : IObservable<object>, IDisposable public interface IPropertyAccessor : IDisposable
{ {
/// <summary> /// <summary>
/// Gets the type of the property. /// Gets the type of the property.
@ -38,5 +37,16 @@ namespace Avalonia.Data.Core.Plugins
/// True if the property was set; false if the property could not be set. /// True if the property was set; false if the property could not be set.
/// </returns> /// </returns>
bool SetValue(object value, BindingPriority priority); bool SetValue(object value, BindingPriority priority);
/// <summary>
/// Subscribes to the value of the member.
/// </summary>
/// <param name="listener">A method that receives the values.</param>
void Subscribe(Action<object> listener);
/// <summary>
/// Unsubscribes to the value of the member.
/// </summary>
void Unsubscribe();
} }
} }

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

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using Avalonia.Data;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Data.Core.Plugins namespace Avalonia.Data.Core.Plugins
@ -40,43 +39,43 @@ namespace Avalonia.Data.Core.Plugins
{ {
if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName)) if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName))
{ {
Observer.OnNext(CreateBindingNotification(Value)); PublishValue(CreateBindingNotification(Value));
} }
} }
protected override void Dispose(bool disposing) protected override void SubscribeCore()
{ {
base.Dispose(disposing);
var target = _reference.Target as INotifyDataErrorInfo; var target = _reference.Target as INotifyDataErrorInfo;
if (target != null) if (target != null)
{ {
WeakSubscriptionManager.Unsubscribe( WeakSubscriptionManager.Subscribe(
target, target,
nameof(target.ErrorsChanged), nameof(target.ErrorsChanged),
this); this);
} }
base.SubscribeCore();
} }
protected override void SubscribeCore(IObserver<object> observer) protected override void UnsubscribeCore()
{ {
var target = _reference.Target as INotifyDataErrorInfo; var target = _reference.Target as INotifyDataErrorInfo;
if (target != null) if (target != null)
{ {
WeakSubscriptionManager.Subscribe( WeakSubscriptionManager.Unsubscribe(
target, target,
nameof(target.ErrorsChanged), nameof(target.ErrorsChanged),
this); this);
} }
base.SubscribeCore(observer); base.UnsubscribeCore();
} }
protected override void InnerValueChanged(object value) protected override void InnerValueChanged(object value)
{ {
base.InnerValueChanged(CreateBindingNotification(value)); PublishValue(CreateBindingNotification(value));
} }
private BindingNotification CreateBindingNotification(object value) private BindingNotification CreateBindingNotification(object value)

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

@ -103,7 +103,13 @@ namespace Avalonia.Data.Core.Plugins
} }
} }
protected override void Dispose(bool disposing) protected override void SubscribeCore()
{
SendCurrentValue();
SubscribeToChanges();
}
protected override void UnsubscribeCore()
{ {
var inpc = _reference.Target as INotifyPropertyChanged; var inpc = _reference.Target as INotifyPropertyChanged;
@ -116,18 +122,12 @@ namespace Avalonia.Data.Core.Plugins
} }
} }
protected override void SubscribeCore(IObserver<object> observer)
{
SendCurrentValue();
SubscribeToChanges();
}
private void SendCurrentValue() private void SendCurrentValue()
{ {
try try
{ {
var value = Value; var value = Value;
Observer.OnNext(value); PublishValue(value);
} }
catch { } catch { }
} }

8
src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs

@ -74,14 +74,18 @@ namespace Avalonia.Data.Core.Plugins
public override bool SetValue(object value, BindingPriority priority) => false; public override bool SetValue(object value, BindingPriority priority) => false;
protected override void SubscribeCore(IObserver<object> observer) protected override void SubscribeCore()
{ {
try try
{ {
Observer.OnNext(Value); PublishValue(Value);
} }
catch { } catch { }
} }
protected override void UnsubscribeCore()
{
}
} }
} }
} }

68
src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs

@ -2,67 +2,75 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using Avalonia.Data;
namespace Avalonia.Data.Core.Plugins namespace Avalonia.Data.Core.Plugins
{ {
/// <summary> /// <summary>
/// Defines a default base implementation for a <see cref="IPropertyAccessor"/>. /// Defines a default base implementation for a <see cref="IPropertyAccessor"/>.
/// </summary> /// </summary>
/// <remarks>
/// <see cref="IPropertyAccessor"/> is an observable that will only be subscribed to one time.
/// In addition, the subscription can be disposed by calling <see cref="Dispose()"/> on the
/// property accessor itself - this prevents needing to hold two references for a subscription.
/// </remarks>
public abstract class PropertyAccessorBase : IPropertyAccessor public abstract class PropertyAccessorBase : IPropertyAccessor
{ {
private Action<object> _listener;
/// <inheritdoc/> /// <inheritdoc/>
public abstract Type PropertyType { get; } public abstract Type PropertyType { get; }
/// <inheritdoc/> /// <inheritdoc/>
public abstract object Value { get; } public abstract object Value { get; }
/// <summary> /// <inheritdoc/>
/// Stops the subscription. public void Dispose()
/// </summary> {
public void Dispose() => Dispose(true); if (_listener != null)
{
Unsubscribe();
}
}
/// <inheritdoc/> /// <inheritdoc/>
public abstract bool SetValue(object value, BindingPriority priority); public abstract bool SetValue(object value, BindingPriority priority);
/// <summary>
/// The currently subscribed observer.
/// </summary>
protected IObserver<object> Observer { get; private set; }
/// <inheritdoc/> /// <inheritdoc/>
public IDisposable Subscribe(IObserver<object> observer) public void Subscribe(Action<object> listener)
{ {
Contract.Requires<ArgumentNullException>(observer != null); Contract.Requires<ArgumentNullException>(listener != null);
if (Observer != null) if (_listener != null)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
"A property accessor can be subscribed to only once."); "A member accessor can be subscribed to only once.");
} }
Observer = observer; _listener = listener;
SubscribeCore(observer); SubscribeCore();
return this;
} }
public void Unsubscribe()
{
if (_listener == null)
{
throw new InvalidOperationException(
"The member accessor was not subscribed.");
}
UnsubscribeCore();
_listener = null;
}
/// <summary>
/// Publishes a value to the listener.
/// </summary>
/// <param name="value">The value.</param>
protected void PublishValue(object value) => _listener?.Invoke(value);
/// <summary> /// <summary>
/// Stops listening to the property. /// When overridden in a derived class, begins listening to the member.
/// </summary> /// </summary>
/// <param name="disposing"> protected abstract void SubscribeCore();
/// True if the <see cref="Dispose()"/> method was called, false if the object is being
/// finalized.
/// </param>
protected virtual void Dispose(bool disposing) => Observer = null;
/// <summary> /// <summary>
/// When overridden in a derived class, begins listening to the property. /// When overridden in a derived class, stops listening to the member.
/// </summary> /// </summary>
protected abstract void SubscribeCore(IObserver<object> observer); protected abstract void UnsubscribeCore();
} }
} }

11
src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs

@ -1,6 +1,4 @@
using System; using System;
using System.Reactive.Disposables;
using Avalonia.Data;
namespace Avalonia.Data.Core.Plugins namespace Avalonia.Data.Core.Plugins
{ {
@ -37,10 +35,13 @@ namespace Avalonia.Data.Core.Plugins
return false; return false;
} }
public IDisposable Subscribe(IObserver<object> observer) public void Subscribe(Action<object> listener)
{
listener(_error);
}
public void Unsubscribe()
{ {
observer.OnNext(_error);
return Disposable.Empty;
} }
} }
} }

29
src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs

@ -3,9 +3,7 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Data;
using Avalonia.Data.Core.Plugins; using Avalonia.Data.Core.Plugins;
namespace Avalonia.Data.Core namespace Avalonia.Data.Core
@ -39,7 +37,7 @@ namespace Avalonia.Data.Core
return false; return false;
} }
protected override IObservable<object> StartListeningCore(WeakReference reference) protected override void StartListeningCore(WeakReference reference)
{ {
var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference.Target, PropertyName)); var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference.Target, PropertyName));
var accessor = plugin?.Start(reference, PropertyName); var accessor = plugin?.Start(reference, PropertyName);
@ -55,17 +53,20 @@ namespace Avalonia.Data.Core
} }
} }
// Ensure that _accessor is set for the duration of the subscription. if (accessor == null)
return Observable.Using( {
() => throw new NotSupportedException(
{ $"Could not find a matching property accessor for {PropertyName}.");
_accessor = accessor; }
return Disposable.Create(() =>
{ accessor.Subscribe(ValueChanged);
_accessor = null; _accessor = accessor;
}); }
},
_ => accessor); protected override void StopListeningCore()
{
_accessor.Dispose();
_accessor = null;
} }
} }
} }

17
src/Avalonia.Base/Data/Core/StreamNode.cs

@ -2,30 +2,37 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Globalization;
using Avalonia.Data;
using System.Reactive.Linq; using System.Reactive.Linq;
namespace Avalonia.Data.Core namespace Avalonia.Data.Core
{ {
internal class StreamNode : ExpressionNode internal class StreamNode : ExpressionNode
{ {
private IDisposable _subscription;
public override string Description => "^"; public override string Description => "^";
protected override IObservable<object> StartListeningCore(WeakReference reference) protected override void StartListeningCore(WeakReference reference)
{ {
foreach (var plugin in ExpressionObserver.StreamHandlers) foreach (var plugin in ExpressionObserver.StreamHandlers)
{ {
if (plugin.Match(reference)) if (plugin.Match(reference))
{ {
return plugin.Start(reference); _subscription = plugin.Start(reference).Subscribe(ValueChanged);
return;
} }
} }
// TODO: Improve error. // TODO: Improve error.
return Observable.Return(new BindingNotification( ValueChanged(new BindingNotification(
new MarkupBindingChainException("Stream operator applied to unsupported type", Description), new MarkupBindingChainException("Stream operator applied to unsupported type", Description),
BindingErrorType.Error)); BindingErrorType.Error));
} }
protected override void StopListeningCore()
{
_subscription?.Dispose();
_subscription = null;
}
} }
} }

42
src/Avalonia.Base/Reactive/AvaloniaObservable.cs

@ -1,42 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive;
using System.Reactive.Disposables;
namespace Avalonia.Reactive
{
/// <summary>
/// An <see cref="IObservable{T}"/> with an additional description.
/// </summary>
/// <typeparam name="T">The type of the elements in the sequence.</typeparam>
public class AvaloniaObservable<T> : ObservableBase<T>, IDescription
{
private readonly Func<IObserver<T>, IDisposable> _subscribe;
/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaObservable{T}"/> class.
/// </summary>
/// <param name="subscribe">The subscribe function.</param>
/// <param name="description">The description of the observable.</param>
public AvaloniaObservable(Func<IObserver<T>, IDisposable> subscribe, string description)
{
Contract.Requires<ArgumentNullException>(subscribe != null);
_subscribe = subscribe;
Description = description;
}
/// <summary>
/// Gets the description of the observable.
/// </summary>
public string Description { get; }
/// <inheritdoc/>
protected override IDisposable SubscribeCore(IObserver<T> observer)
{
return _subscribe(observer) ?? Disposable.Empty;
}
}
}

46
src/Avalonia.Base/Reactive/AvaloniaPropertyChangedObservable.cs

@ -0,0 +1,46 @@
using System;
namespace Avalonia.Reactive
{
internal class AvaloniaPropertyChangedObservable :
LightweightObservableBase<AvaloniaPropertyChangedEventArgs>,
IDescription
{
private readonly WeakReference<IAvaloniaObject> _target;
private readonly AvaloniaProperty _property;
public AvaloniaPropertyChangedObservable(
IAvaloniaObject target,
AvaloniaProperty property)
{
_target = new WeakReference<IAvaloniaObject>(target);
_property = property;
}
public string Description => $"{_target.GetType().Name}.{_property.Name}";
protected override void Initialize()
{
if (_target.TryGetTarget(out var target))
{
target.PropertyChanged += PropertyChanged;
}
}
protected override void Deinitialize()
{
if (_target.TryGetTarget(out var target))
{
target.PropertyChanged -= PropertyChanged;
}
}
private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == _property)
{
PublishNext(e);
}
}
}
}

52
src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs

@ -0,0 +1,52 @@
using System;
namespace Avalonia.Reactive
{
internal class AvaloniaPropertyObservable<T> : LightweightObservableBase<T>, IDescription
{
private readonly WeakReference<IAvaloniaObject> _target;
private readonly AvaloniaProperty _property;
private T _value;
public AvaloniaPropertyObservable(
IAvaloniaObject target,
AvaloniaProperty property)
{
_target = new WeakReference<IAvaloniaObject>(target);
_property = property;
}
public string Description => $"{_target.GetType().Name}.{_property.Name}";
protected override void Initialize()
{
if (_target.TryGetTarget(out var target))
{
_value = (T)target.GetValue(_property);
target.PropertyChanged += PropertyChanged;
}
}
protected override void Deinitialize()
{
if (_target.TryGetTarget(out var target))
{
target.PropertyChanged -= PropertyChanged;
}
}
protected override void Subscribed(IObserver<T> observer, bool first)
{
observer.OnNext(_value);
}
private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == _property)
{
_value = (T)e.NewValue;
PublishNext(_value);
}
}
}
}

202
src/Avalonia.Base/Reactive/LightweightObservableBase.cs

@ -0,0 +1,202 @@
using System;
using System.Collections.Generic;
using System.Reactive;
using System.Reactive.Disposables;
using System.Threading;
using Avalonia.Threading;
namespace Avalonia.Reactive
{
/// <summary>
/// Lightweight base class for observable implementations.
/// </summary>
/// <typeparam name="T">The observable type.</typeparam>
/// <remarks>
/// <see cref="ObservableBase{T}"/> is rather heavyweight in terms of allocations and memory
/// usage. This class provides a more lightweight base for some internal observable types
/// in the Avalonia framework.
/// </remarks>
public abstract class LightweightObservableBase<T> : IObservable<T>
{
private Exception _error;
private List<IObserver<T>> _observers = new List<IObserver<T>>();
public IDisposable Subscribe(IObserver<T> observer)
{
Contract.Requires<ArgumentNullException>(observer != null);
Dispatcher.UIThread.VerifyAccess();
var first = false;
for (; ; )
{
if (Volatile.Read(ref _observers) == null)
{
if (_error != null)
{
observer.OnError(_error);
}
else
{
observer.OnCompleted();
}
return Disposable.Empty;
}
lock (this)
{
if (_observers == null)
{
continue;
}
first = _observers.Count == 0;
_observers.Add(observer);
break;
}
}
if (first)
{
Initialize();
}
Subscribed(observer, first);
return new RemoveObserver(this, observer);
}
void Remove(IObserver<T> observer)
{
if (Volatile.Read(ref _observers) != null)
{
lock (this)
{
var observers = _observers;
if (observers != null)
{
observers.Remove(observer);
if (observers.Count == 0)
{
observers.TrimExcess();
}
else
{
return;
}
} else
{
return;
}
}
Deinitialize();
}
}
sealed class RemoveObserver : IDisposable
{
LightweightObservableBase<T> _parent;
IObserver<T> _observer;
public RemoveObserver(LightweightObservableBase<T> parent, IObserver<T> observer)
{
_parent = parent;
Volatile.Write(ref _observer, observer);
}
public void Dispose()
{
var observer = _observer;
Interlocked.Exchange(ref _parent, null)?.Remove(observer);
_observer = null;
}
}
protected abstract void Initialize();
protected abstract void Deinitialize();
protected void PublishNext(T value)
{
if (Volatile.Read(ref _observers) != null)
{
IObserver<T>[] observers;
lock (this)
{
if (_observers == null)
{
return;
}
observers = _observers.ToArray();
}
foreach (var observer in observers)
{
observer.OnNext(value);
}
}
}
protected void PublishCompleted()
{
if (Volatile.Read(ref _observers) != null)
{
IObserver<T>[] observers;
lock (this)
{
if (_observers == null)
{
return;
}
observers = _observers.ToArray();
Volatile.Write(ref _observers, null);
}
foreach (var observer in observers)
{
observer.OnCompleted();
}
Deinitialize();
}
}
protected void PublishError(Exception error)
{
if (Volatile.Read(ref _observers) != null)
{
IObserver<T>[] observers;
lock (this)
{
if (_observers == null)
{
return;
}
_error = error;
observers = _observers.ToArray();
Volatile.Write(ref _observers, null);
}
foreach (var observer in observers)
{
observer.OnError(error);
}
Deinitialize();
}
}
protected virtual void Subscribed(IObserver<T> observer, bool first)
{
}
}
}

76
src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs

@ -0,0 +1,76 @@
using System;
using Avalonia.Threading;
namespace Avalonia.Reactive
{
public abstract class SingleSubscriberObservableBase<T> : IObservable<T>, IDisposable
{
private Exception _error;
private IObserver<T> _observer;
private bool _completed;
public IDisposable Subscribe(IObserver<T> observer)
{
Contract.Requires<ArgumentNullException>(observer != null);
Dispatcher.UIThread.VerifyAccess();
if (_observer != null)
{
throw new InvalidOperationException("The observable can only be subscribed once.");
}
if (_error != null)
{
observer.OnError(_error);
}
else if (_completed)
{
observer.OnCompleted();
}
else
{
_observer = observer;
Subscribed();
}
return this;
}
void IDisposable.Dispose()
{
Unsubscribed();
_observer = null;
}
protected abstract void Unsubscribed();
protected void PublishNext(T value)
{
_observer?.OnNext(value);
}
protected void PublishCompleted()
{
if (_observer != null)
{
_observer.OnCompleted();
_completed = true;
Unsubscribed();
_observer = null;
}
}
protected void PublishError(Exception error)
{
if (_observer != null)
{
_observer.OnError(error);
_error = error;
Unsubscribed();
_observer = null;
}
}
protected abstract void Subscribed();
}
}

85
src/Avalonia.Base/Reactive/WeakPropertyChangedObservable.cs

@ -1,85 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Utilities;
namespace Avalonia.Reactive
{
internal class WeakPropertyChangedObservable : ObservableBase<object>,
IWeakSubscriber<AvaloniaPropertyChangedEventArgs>, IDescription
{
private WeakReference<IAvaloniaObject> _sourceReference;
private readonly AvaloniaProperty _property;
private readonly Subject<object> _changed = new Subject<object>();
private int _count;
public WeakPropertyChangedObservable(
WeakReference<IAvaloniaObject> source,
AvaloniaProperty property,
string description)
{
_sourceReference = source;
_property = property;
Description = description;
}
public string Description { get; }
public void OnEvent(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == _property)
{
_changed.OnNext(e.NewValue);
}
}
protected override IDisposable SubscribeCore(IObserver<object> observer)
{
IAvaloniaObject instance;
if (_sourceReference.TryGetTarget(out instance))
{
if (_count++ == 0)
{
WeakSubscriptionManager.Subscribe(
instance,
nameof(instance.PropertyChanged),
this);
}
observer.OnNext(instance.GetValue(_property));
return Observable.Using(() => Disposable.Create(DecrementCount), _ => _changed)
.Subscribe(observer);
}
else
{
_changed.OnCompleted();
observer.OnCompleted();
return Disposable.Empty;
}
}
private void DecrementCount()
{
if (--_count == 0)
{
IAvaloniaObject instance;
if (_sourceReference.TryGetTarget(out instance))
{
WeakSubscriptionManager.Unsubscribe(
instance,
nameof(instance.PropertyChanged),
this);
}
}
}
}
}

1
src/Avalonia.Controls/ItemsControl.cs

@ -155,6 +155,7 @@ namespace Avalonia.Controls
void IItemsPresenterHost.RegisterItemsPresenter(IItemsPresenter presenter) void IItemsPresenterHost.RegisterItemsPresenter(IItemsPresenter presenter)
{ {
Presenter = presenter; Presenter = presenter;
ItemContainerGenerator.Clear();
} }
/// <summary> /// <summary>

40
src/Avalonia.Controls/Mixins/ContentControlMixin.cs

@ -49,11 +49,9 @@ namespace Avalonia.Controls.Mixins
Contract.Requires<ArgumentNullException>(content != null); Contract.Requires<ArgumentNullException>(content != null);
Contract.Requires<ArgumentNullException>(logicalChildrenSelector != null); Contract.Requires<ArgumentNullException>(logicalChildrenSelector != null);
EventHandler<RoutedEventArgs> templateApplied = (s, ev) => void TemplateApplied(object s, RoutedEventArgs ev)
{ {
var sender = s as TControl; if (s is TControl sender)
if (sender != null)
{ {
var e = (TemplateAppliedEventArgs)ev; var e = (TemplateAppliedEventArgs)ev;
var presenter = (IControl)e.NameScope.Find(presenterName); var presenter = (IControl)e.NameScope.Find(presenterName);
@ -64,12 +62,12 @@ namespace Avalonia.Controls.Mixins
var logicalChildren = logicalChildrenSelector(sender); var logicalChildren = logicalChildrenSelector(sender);
var subscription = presenter var subscription = presenter
.GetObservableWithHistory(ContentPresenter.ChildProperty) .GetPropertyChangedObservable(ContentPresenter.ChildProperty)
.Subscribe(child => UpdateLogicalChild( .Subscribe(c => UpdateLogicalChild(
sender, sender,
logicalChildren, logicalChildren,
child.Item1, c.OldValue,
child.Item2)); c.NewValue));
UpdateLogicalChild( UpdateLogicalChild(
sender, sender,
@ -80,18 +78,16 @@ namespace Avalonia.Controls.Mixins
subscriptions.Value.Add(sender, subscription); subscriptions.Value.Add(sender, subscription);
} }
} }
}; }
TemplatedControl.TemplateAppliedEvent.AddClassHandler( TemplatedControl.TemplateAppliedEvent.AddClassHandler(
typeof(TControl), typeof(TControl),
templateApplied, TemplateApplied,
RoutingStrategies.Direct); RoutingStrategies.Direct);
content.Changed.Subscribe(e => content.Changed.Subscribe(e =>
{ {
var sender = e.Sender as TControl; if (e.Sender is TControl sender)
if (sender != null)
{ {
var logicalChildren = logicalChildrenSelector(sender); var logicalChildren = logicalChildrenSelector(sender);
UpdateLogicalChild(sender, logicalChildren, e.OldValue, e.NewValue); UpdateLogicalChild(sender, logicalChildren, e.OldValue, e.NewValue);
@ -100,9 +96,7 @@ namespace Avalonia.Controls.Mixins
Control.TemplatedParentProperty.Changed.Subscribe(e => Control.TemplatedParentProperty.Changed.Subscribe(e =>
{ {
var sender = e.Sender as TControl; if (e.Sender is TControl sender)
if (sender != null)
{ {
var logicalChild = logicalChildrenSelector(sender).FirstOrDefault() as IControl; var logicalChild = logicalChildrenSelector(sender).FirstOrDefault() as IControl;
logicalChild?.SetValue(Control.TemplatedParentProperty, sender.TemplatedParent); logicalChild?.SetValue(Control.TemplatedParentProperty, sender.TemplatedParent);
@ -111,13 +105,9 @@ namespace Avalonia.Controls.Mixins
TemplatedControl.TemplateProperty.Changed.Subscribe(e => TemplatedControl.TemplateProperty.Changed.Subscribe(e =>
{ {
var sender = e.Sender as TControl; if (e.Sender is TControl sender)
if (sender != null)
{ {
IDisposable subscription; if (subscriptions.Value.TryGetValue(sender, out IDisposable subscription))
if (subscriptions.Value.TryGetValue(sender, out subscription))
{ {
subscription.Dispose(); subscription.Dispose();
subscriptions.Value.Remove(sender); subscriptions.Value.Remove(sender);
@ -134,9 +124,7 @@ namespace Avalonia.Controls.Mixins
{ {
if (oldValue != newValue) if (oldValue != newValue)
{ {
var child = oldValue as IControl; if (oldValue is IControl child)
if (child != null)
{ {
logicalChildren.Remove(child); logicalChildren.Remove(child);
} }

11
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -408,12 +408,15 @@ namespace Avalonia.Controls.Primitives
var panel = (InputElement)Presenter.Panel; var panel = (InputElement)Presenter.Panel;
foreach (var container in e.Containers) if (panel != null)
{ {
if (KeyboardNavigation.GetTabOnceActiveElement(panel) == container.ContainerControl) foreach (var container in e.Containers)
{ {
KeyboardNavigation.SetTabOnceActiveElement(panel, null); if (KeyboardNavigation.GetTabOnceActiveElement(panel) == container.ContainerControl)
break; {
KeyboardNavigation.SetTabOnceActiveElement(panel, null);
break;
}
} }
} }
} }

1
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@ -247,6 +247,7 @@ namespace Avalonia.Controls.Primitives
foreach (var child in this.GetTemplateChildren()) foreach (var child in this.GetTemplateChildren())
{ {
child.SetValue(TemplatedParentProperty, null); child.SetValue(TemplatedParentProperty, null);
((ISetLogicalParent)child).SetParent(null);
} }
VisualChildren.Clear(); VisualChildren.Clear();

39
src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Reactive; using System.Reactive;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Reactive;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -55,11 +56,39 @@ namespace Avalonia.Controls
public static IObservable<object> GetResourceObservable(this IResourceNode target, string key) public static IObservable<object> GetResourceObservable(this IResourceNode target, string key)
{ {
return Observable.FromEventPattern<ResourcesChangedEventArgs>( return new ResourceObservable(target, key);
x => target.ResourcesChanged += x, }
x => target.ResourcesChanged -= x)
.StartWith((EventPattern<ResourcesChangedEventArgs>)null) private class ResourceObservable : LightweightObservableBase<object>
.Select(x => target.FindResource(key)); {
private readonly IResourceNode _target;
private readonly string _key;
public ResourceObservable(IResourceNode target, string key)
{
_target = target;
_key = key;
}
protected override void Initialize()
{
_target.ResourcesChanged += ResourcesChanged;
}
protected override void Deinitialize()
{
_target.ResourcesChanged -= ResourcesChanged;
}
protected override void Subscribed(IObserver<object> observer, bool first)
{
observer.OnNext(_target.FindResource(_key));
}
private void ResourcesChanged(object sender, ResourcesChangedEventArgs e)
{
PublishNext(_target.FindResource(_key));
}
} }
} }
} }

162
src/Avalonia.Styling/LogicalTree/ControlLocator.cs

@ -6,6 +6,7 @@ using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reflection; using System.Reflection;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Reactive;
namespace Avalonia.LogicalTree namespace Avalonia.LogicalTree
{ {
@ -23,75 +24,122 @@ namespace Avalonia.LogicalTree
/// <param name="name">The name of the control to find.</param> /// <param name="name">The name of the control to find.</param>
public static IObservable<ILogical> Track(ILogical relativeTo, string name) public static IObservable<ILogical> Track(ILogical relativeTo, string name)
{ {
var attached = Observable.FromEventPattern<LogicalTreeAttachmentEventArgs>( return new ControlTracker(relativeTo, name);
x => relativeTo.AttachedToLogicalTree += x, }
x => relativeTo.AttachedToLogicalTree -= x)
.Select(x => ((ILogical)x.Sender).FindNameScope()) public static IObservable<ILogical> Track(ILogical relativeTo, int ancestorLevel, Type ancestorType = null)
.StartWith(relativeTo.FindNameScope()); {
return new ControlTracker(relativeTo, ancestorLevel, ancestorType);
var detached = Observable.FromEventPattern<LogicalTreeAttachmentEventArgs>( }
x => relativeTo.DetachedFromLogicalTree += x,
x => relativeTo.DetachedFromLogicalTree -= x) private class ControlTracker : LightweightObservableBase<ILogical>
.Select(x => (INameScope)null); {
private readonly ILogical _relativeTo;
return attached.Merge(detached).Select(nameScope => private readonly string _name;
private readonly int _ancestorLevel;
private readonly Type _ancestorType;
INameScope _nameScope;
ILogical _value;
public ControlTracker(ILogical relativeTo, string name)
{
_relativeTo = relativeTo;
_name = name;
}
public ControlTracker(ILogical relativeTo, int ancestorLevel, Type ancestorType)
{ {
if (nameScope != null) _relativeTo = relativeTo;
_ancestorLevel = ancestorLevel;
_ancestorType = ancestorType;
}
protected override void Initialize()
{
Update();
_relativeTo.AttachedToLogicalTree += Attached;
_relativeTo.DetachedFromLogicalTree += Detached;
}
protected override void Deinitialize()
{
_relativeTo.AttachedToLogicalTree -= Attached;
_relativeTo.DetachedFromLogicalTree -= Detached;
if (_nameScope != null)
{ {
var registered = Observable.FromEventPattern<NameScopeEventArgs>( _nameScope.Registered -= Registered;
x => nameScope.Registered += x, _nameScope.Unregistered -= Unregistered;
x => nameScope.Registered -= x)
.Where(x => x.EventArgs.Name == name)
.Select(x => x.EventArgs.Element)
.OfType<ILogical>();
var unregistered = Observable.FromEventPattern<NameScopeEventArgs>(
x => nameScope.Unregistered += x,
x => nameScope.Unregistered -= x)
.Where(x => x.EventArgs.Name == name)
.Select(_ => (ILogical)null);
return registered
.StartWith(nameScope.Find<ILogical>(name))
.Merge(unregistered);
} }
else
_value = null;
}
protected override void Subscribed(IObserver<ILogical> observer, bool first)
{
observer.OnNext(_value);
}
private void Attached(object sender, LogicalTreeAttachmentEventArgs e)
{
Update();
PublishNext(_value);
}
private void Detached(object sender, LogicalTreeAttachmentEventArgs e)
{
if (_nameScope != null)
{ {
return Observable.Return<ILogical>(null); _nameScope.Registered -= Registered;
_nameScope.Unregistered -= Unregistered;
} }
}).Switch();
}
public static IObservable<ILogical> Track(ILogical relativeTo, int ancestorLevel, Type ancestorType = null) _value = null;
{ PublishNext(null);
return TrackAttachmentToTree(relativeTo).Select(isAttachedToTree => }
private void Registered(object sender, NameScopeEventArgs e)
{ {
if (isAttachedToTree) if (e.Name == _name && e.Element is ILogical logical)
{ {
return relativeTo.GetLogicalAncestors() _value = logical;
.Where(x => ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) PublishNext(logical);
.ElementAtOrDefault(ancestorLevel);
} }
else }
private void Unregistered(object sender, NameScopeEventArgs e)
{
if (e.Name == _name)
{ {
return null; _value = null;
PublishNext(null);
} }
}); }
}
private static IObservable<bool> TrackAttachmentToTree(ILogical relativeTo) private void Update()
{ {
var attached = Observable.FromEventPattern<LogicalTreeAttachmentEventArgs>( if (_name != null)
x => relativeTo.AttachedToLogicalTree += x, {
x => relativeTo.AttachedToLogicalTree -= x) _nameScope = _relativeTo.FindNameScope();
.Select(x => true)
.StartWith(relativeTo.IsAttachedToLogicalTree); if (_nameScope != null)
{
var detached = Observable.FromEventPattern<LogicalTreeAttachmentEventArgs>( _nameScope.Registered += Registered;
x => relativeTo.DetachedFromLogicalTree += x, _nameScope.Unregistered += Unregistered;
x => relativeTo.DetachedFromLogicalTree -= x) _value = _nameScope.Find<ILogical>(_name);
.Select(x => false); }
else
var attachmentStatus = attached.Merge(detached); {
return attachmentStatus; _value = null;
}
}
else
{
_value = _relativeTo.GetLogicalAncestors()
.Where(x => _ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true)
.ElementAtOrDefault(_ancestorLevel);
}
}
} }
} }
} }

66
src/Avalonia.Styling/Styling/ActivatedObservable.cs

@ -2,8 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Reactive;
using System.Reactive.Linq;
namespace Avalonia.Styling namespace Avalonia.Styling
{ {
@ -11,14 +9,16 @@ namespace Avalonia.Styling
/// An observable which is switched on or off according to an activator observable. /// An observable which is switched on or off according to an activator observable.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// An <see cref="ActivatedObservable"/> has two inputs: an activator observable a /// An <see cref="ActivatedObservable"/> has two inputs: an activator observable and a
/// <see cref="Source"/> observable which produces the activated value. When the activator /// <see cref="Source"/> observable which produces the activated value. When the activator
/// produces true, the <see cref="ActivatedObservable"/> will produce the current activated /// produces true, the <see cref="ActivatedObservable"/> will produce the current activated
/// value. When the activator produces false it will produce /// value. When the activator produces false it will produce
/// <see cref="AvaloniaProperty.UnsetValue"/>. /// <see cref="AvaloniaProperty.UnsetValue"/>.
/// </remarks> /// </remarks>
internal class ActivatedObservable : ObservableBase<object>, IDescription internal class ActivatedObservable : ActivatedValue, IDescription
{ {
private IDisposable _sourceSubscription;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ActivatedObservable"/> class. /// Initializes a new instance of the <see cref="ActivatedObservable"/> class.
/// </summary> /// </summary>
@ -29,49 +29,49 @@ namespace Avalonia.Styling
IObservable<bool> activator, IObservable<bool> activator,
IObservable<object> source, IObservable<object> source,
string description) string description)
: base(activator, AvaloniaProperty.UnsetValue, description)
{ {
Contract.Requires<ArgumentNullException>(activator != null);
Contract.Requires<ArgumentNullException>(source != null); Contract.Requires<ArgumentNullException>(source != null);
Activator = activator;
Description = description;
Source = source; Source = source;
} }
/// <summary>
/// Gets the activator observable.
/// </summary>
public IObservable<bool> Activator { get; }
/// <summary>
/// Gets a description of the binding.
/// </summary>
public string Description { get; }
/// <summary> /// <summary>
/// Gets an observable which produces the <see cref="ActivatedValue"/>. /// Gets an observable which produces the <see cref="ActivatedValue"/>.
/// </summary> /// </summary>
public IObservable<object> Source { get; } public IObservable<object> Source { get; }
/// <summary> protected override ActivatorListener CreateListener() => new ValueListener(this);
/// Notifies the provider that an observer is to receive notifications.
/// </summary> protected override void Deinitialize()
/// <param name="observer">The observer.</param>
/// <returns>IDisposable object used to unsubscribe from the observable sequence.</returns>
protected override IDisposable SubscribeCore(IObserver<object> observer)
{ {
Contract.Requires<ArgumentNullException>(observer != null); base.Deinitialize();
_sourceSubscription.Dispose();
_sourceSubscription = null;
}
protected override void Initialize()
{
base.Initialize();
_sourceSubscription = Source.Subscribe((ValueListener)Listener);
}
var sourceCompleted = Source.LastOrDefaultAsync().Select(_ => Unit.Default); protected virtual void NotifyValue(object value)
var activatorCompleted = Activator.LastOrDefaultAsync().Select(_ => Unit.Default); {
var completed = sourceCompleted.Merge(activatorCompleted); Value = value;
}
private class ValueListener : ActivatorListener, IObserver<object>
{
public ValueListener(ActivatedObservable parent)
: base(parent)
{
}
protected new ActivatedObservable Parent => (ActivatedObservable)base.Parent;
return Activator void IObserver<object>.OnCompleted() => Parent.CompletedReceived();
.CombineLatest(Source, (x, y) => new { Active = x, Value = y }) void IObserver<object>.OnError(Exception error) => Parent.ErrorReceived(error);
.Select(x => x.Active ? x.Value : AvaloniaProperty.UnsetValue) void IObserver<object>.OnNext(object value) => Parent.NotifyValue(value);
.DistinctUntilChanged()
.TakeUntil(completed)
.Subscribe(observer);
} }
} }
} }

71
src/Avalonia.Styling/Styling/ActivatedSubject.cs

@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
namespace Avalonia.Styling namespace Avalonia.Styling
@ -11,17 +10,14 @@ namespace Avalonia.Styling
/// A subject which is switched on or off according to an activator observable. /// A subject which is switched on or off according to an activator observable.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// An <see cref="ActivatedSubject"/> has two inputs: an activator observable and either an /// An <see cref="ActivatedSubject"/> extends <see cref="ActivatedObservable"/> to
/// <see cref="ActivatedValue"/> or a <see cref="Source"/> observable which produces the /// be an <see cref="ISubject{Object}"/>. When the object is active then values
/// activated value. When the activator produces true, the <see cref="ActivatedObservable"/> will /// received via <see cref="OnNext(object)"/> will be passed to the source subject.
/// produce the current activated value. When the activator produces false it will produce
/// <see cref="AvaloniaProperty.UnsetValue"/>.
/// </remarks> /// </remarks>
internal class ActivatedSubject : ActivatedObservable, ISubject<object>, IDescription internal class ActivatedSubject : ActivatedObservable, ISubject<object>, IDescription
{ {
private bool? _active;
private bool _completed; private bool _completed;
private object _value; private object _pushValue;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ActivatedSubject"/> class. /// Initializes a new instance of the <see cref="ActivatedSubject"/> class.
@ -35,7 +31,6 @@ namespace Avalonia.Styling
string description) string description)
: base(activator, source, description) : base(activator, source, description)
{ {
Activator.Subscribe(ActivatorChanged, ActivatorError, ActivatorCompleted);
} }
/// <summary> /// <summary>
@ -46,53 +41,57 @@ namespace Avalonia.Styling
get { return (ISubject<object>)base.Source; } get { return (ISubject<object>)base.Source; }
} }
/// <summary>
/// Notifies all subscribed observers about the end of the sequence.
/// </summary>
public void OnCompleted() public void OnCompleted()
{ {
if (_active.Value && !_completed) Source.OnCompleted();
{
Source.OnCompleted();
}
} }
/// <summary>
/// Notifies all subscribed observers with the exception.
/// </summary>
/// <param name="error">The exception to send to all subscribed observers.</param>
/// <exception cref="ArgumentNullException"><paramref name="error"/> is null.</exception>
public void OnError(Exception error) public void OnError(Exception error)
{ {
if (_active.Value && !_completed) Source.OnError(error);
{
Source.OnError(error);
}
} }
/// <summary>
/// Notifies all subscribed observers with the value.
/// </summary>
/// <param name="value">The value to send to all subscribed observers.</param>
public void OnNext(object value) public void OnNext(object value)
{ {
_value = value; _pushValue = value;
if (_active.Value && !_completed) if (IsActive == true && !_completed)
{ {
Source.OnNext(value); Source.OnNext(_pushValue);
} }
} }
private void ActivatorChanged(bool active) protected override void ActiveChanged(bool active)
{ {
bool first = !_active.HasValue; bool first = !IsActive.HasValue;
_active = active; base.ActiveChanged(active);
if (!first) if (!first)
{ {
Source.OnNext(active ? _value : AvaloniaProperty.UnsetValue); Source.OnNext(active ? _pushValue : AvaloniaProperty.UnsetValue);
}
}
protected override void CompletedReceived()
{
base.CompletedReceived();
if (!_completed)
{
Source.OnCompleted();
_completed = true;
}
}
protected override void ErrorReceived(Exception error)
{
base.ErrorReceived(error);
if (!_completed)
{
Source.OnError(error);
_completed = true;
} }
} }

111
src/Avalonia.Styling/Styling/ActivatedValue.cs

@ -2,8 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Reactive; using Avalonia.Reactive;
using System.Reactive.Linq;
namespace Avalonia.Styling namespace Avalonia.Styling
{ {
@ -16,12 +15,12 @@ namespace Avalonia.Styling
/// <see cref="ActivatedValue"/> will produce the current value. When the activator /// <see cref="ActivatedValue"/> will produce the current value. When the activator
/// produces false it will produce <see cref="AvaloniaProperty.UnsetValue"/>. /// produces false it will produce <see cref="AvaloniaProperty.UnsetValue"/>.
/// </remarks> /// </remarks>
internal class ActivatedValue : ObservableBase<object>, IDescription internal class ActivatedValue : LightweightObservableBase<object>, IDescription
{ {
/// <summary> private static readonly object NotSent = new object();
/// The activator. private IDisposable _activatorSubscription;
/// </summary> private object _value;
private readonly IObservable<bool> _activator; private object _last = NotSent;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ActivatedObservable"/> class. /// Initializes a new instance of the <see cref="ActivatedObservable"/> class.
@ -34,39 +33,101 @@ namespace Avalonia.Styling
object value, object value,
string description) string description)
{ {
_activator = activator; Contract.Requires<ArgumentNullException>(activator != null);
Activator = activator;
Value = value; Value = value;
Description = description; Description = description;
Listener = CreateListener();
} }
/// <summary> /// <summary>
/// Gets the activated value. /// Gets the activator observable.
/// </summary> /// </summary>
public object Value public IObservable<bool> Activator { get; }
{
get;
}
/// <summary> /// <summary>
/// Gets a description of the binding. /// Gets a description of the binding.
/// </summary> /// </summary>
public string Description public string Description { get; }
{
get; /// <summary>
} /// Gets a value indicating whether the activator is active.
/// </summary>
public bool? IsActive { get; private set; }
/// <summary> /// <summary>
/// Notifies the provider that an observer is to receive notifications. /// Gets the value that will be produced when <see cref="IsActive"/> is true.
/// </summary> /// </summary>
/// <param name="observer">The observer.</param> public object Value
/// <returns>IDisposable object used to unsubscribe from the observable sequence.</returns> {
protected override IDisposable SubscribeCore(IObserver<object> observer) get => _value;
protected set
{
_value = value;
PublishValue();
}
}
protected ActivatorListener Listener { get; }
protected virtual void ActiveChanged(bool active)
{
IsActive = active;
PublishValue();
}
protected virtual void CompletedReceived() => PublishCompleted();
protected virtual ActivatorListener CreateListener() => new ActivatorListener(this);
protected override void Deinitialize()
{
_activatorSubscription.Dispose();
_activatorSubscription = null;
}
protected virtual void ErrorReceived(Exception error) => PublishError(error);
protected override void Initialize()
{
_activatorSubscription = Activator.Subscribe(Listener);
}
protected override void Subscribed(IObserver<object> observer, bool first)
{ {
Contract.Requires<ArgumentNullException>(observer != null); if (IsActive == true && !first)
{
observer.OnNext(Value);
}
}
private void PublishValue()
{
if (IsActive.HasValue)
{
var v = IsActive.Value ? Value : AvaloniaProperty.UnsetValue;
if (!Equals(v, _last))
{
PublishNext(v);
_last = v;
}
}
}
protected class ActivatorListener : IObserver<bool>
{
public ActivatorListener(ActivatedValue parent)
{
Parent = parent;
}
protected ActivatedValue Parent { get; }
return _activator void IObserver<bool>.OnCompleted() => Parent.CompletedReceived();
.Select(active => active ? Value : AvaloniaProperty.UnsetValue) void IObserver<bool>.OnError(Exception error) => Parent.ErrorReceived(error);
.Subscribe(observer); void IObserver<bool>.OnNext(bool value) => Parent.ActiveChanged(value);
} }
} }
} }

4
src/Avalonia.Styling/Styling/StyleActivator.cs

@ -48,8 +48,8 @@ namespace Avalonia.Styling
else else
{ {
return inputs.CombineLatest() return inputs.CombineLatest()
.Select(values => values.Any(x => x)) .Select(values => values.Any(x => x))
.DistinctUntilChanged(); .DistinctUntilChanged();
} }
} }
} }

68
src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs

@ -4,10 +4,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Reactive;
using System.Reactive.Linq;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using Avalonia.Collections;
using Avalonia.Reactive;
namespace Avalonia.Styling namespace Avalonia.Styling
{ {
@ -122,14 +122,7 @@ namespace Avalonia.Styling
{ {
if (subscribe) if (subscribe)
{ {
var observable = Observable.FromEventPattern< var observable = new ClassObserver(control.Classes, _classes.Value);
NotifyCollectionChangedEventHandler,
NotifyCollectionChangedEventArgs>(
x => control.Classes.CollectionChanged += x,
x => control.Classes.CollectionChanged -= x)
.StartWith((EventPattern<NotifyCollectionChangedEventArgs>)null)
.Select(_ => Matches(control.Classes))
.DistinctUntilChanged();
return new SelectorMatch(observable); return new SelectorMatch(observable);
} }
else else
@ -204,5 +197,60 @@ namespace Avalonia.Styling
return builder.ToString(); return builder.ToString();
} }
private class ClassObserver : LightweightObservableBase<bool>
{
readonly IList<string> _match;
IAvaloniaReadOnlyList<string> _classes;
bool _value;
public ClassObserver(IAvaloniaReadOnlyList<string> classes, IList<string> match)
{
_classes = classes;
_match = match;
}
protected override void Deinitialize() => _classes.CollectionChanged -= ClassesChanged;
protected override void Initialize()
{
_value = GetResult();
_classes.CollectionChanged += ClassesChanged;
}
protected override void Subscribed(IObserver<bool> observer, bool first)
{
observer.OnNext(_value);
}
private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action != NotifyCollectionChangedAction.Move)
{
var value = GetResult();
if (value != _value)
{
PublishNext(GetResult());
_value = value;
}
}
}
private bool GetResult()
{
int remaining = _match.Count;
foreach (var c in _classes)
{
if (_match.Contains(c))
{
--remaining;
}
}
return remaining == 0;
}
}
} }
} }

1
src/Avalonia.Visuals/Avalonia.Visuals.csproj

@ -8,4 +8,5 @@
<ProjectReference Include="..\Avalonia.Styling\Avalonia.Styling.csproj" /> <ProjectReference Include="..\Avalonia.Styling\Avalonia.Styling.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\Rx.props" /> <Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\System.Memory.props" />
</Project> </Project>

670
src/Avalonia.Visuals/Media/PathMarkupParser.cs

@ -5,9 +5,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Avalonia.Platform; using Avalonia.Platform;
namespace Avalonia.Media namespace Avalonia.Media
@ -17,7 +14,6 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public class PathMarkupParser : IDisposable public class PathMarkupParser : IDisposable
{ {
private static readonly string s_separatorPattern;
private static readonly Dictionary<char, Command> s_commands = private static readonly Dictionary<char, Command> s_commands =
new Dictionary<char, Command> new Dictionary<char, Command>
{ {
@ -37,14 +33,9 @@ namespace Avalonia.Media
private IGeometryContext _geometryContext; private IGeometryContext _geometryContext;
private Point _currentPoint; private Point _currentPoint;
private Point? _previousControlPoint; private Point? _previousControlPoint;
private bool? _isOpen; private bool _isOpen;
private bool _isDisposed; private bool _isDisposed;
static PathMarkupParser()
{
s_separatorPattern = CreatesSeparatorPattern();
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PathMarkupParser"/> class. /// Initializes a new instance of the <see cref="PathMarkupParser"/> class.
/// </summary> /// </summary>
@ -76,18 +67,6 @@ namespace Avalonia.Media
Close Close
} }
/// <summary>
/// Parses the specified path data and writes the result to the geometryContext of this instance.
/// </summary>
/// <param name="pathData">The path data.</param>
public void Parse(string pathData)
{
var normalizedPathData = NormalizeWhiteSpaces(pathData);
var tokens = ParseTokens(normalizedPathData);
CreateGeometry(tokens);
}
void IDisposable.Dispose() void IDisposable.Dispose()
{ {
Dispose(true); Dispose(true);
@ -108,66 +87,6 @@ namespace Avalonia.Media
_isDisposed = true; _isDisposed = true;
} }
private static string NormalizeWhiteSpaces(string s)
{
int length = s.Length,
index = 0,
i = 0;
var source = s.ToCharArray();
var skip = false;
for (; i < length; i++)
{
var c = source[i];
if (char.IsWhiteSpace(c))
{
if (skip)
{
continue;
}
source[index++] = c;
skip = true;
continue;
}
skip = false;
source[index++] = c;
}
if (char.IsWhiteSpace(source[index - 1]))
{
index--;
}
return char.IsWhiteSpace(source[0]) ? new string(source, 1, index) : new string(source, 0, index);
}
private static string CreatesSeparatorPattern()
{
var stringBuilder = new StringBuilder();
foreach (var command in s_commands.Keys)
{
stringBuilder.Append(command);
stringBuilder.Append(char.ToLower(command));
}
return @"(?=[" + stringBuilder + "])";
}
private static IEnumerable<CommandToken> ParseTokens(string s)
{
var expressions = Regex.Split(s, s_separatorPattern).Where(t => !string.IsNullOrEmpty(t));
return expressions.Select(CommandToken.Parse);
}
private static Point MirrorControlPoint(Point controlPoint, Point center) private static Point MirrorControlPoint(Point controlPoint, Point center)
{ {
var dir = controlPoint - center; var dir = controlPoint - center;
@ -175,76 +94,78 @@ namespace Avalonia.Media
return center + -dir; return center + -dir;
} }
private void CreateGeometry(IEnumerable<CommandToken> commandTokens) /// <summary>
/// Parses the specified path data and writes the result to the geometryContext of this instance.
/// </summary>
/// <param name="pathData">The path data.</param>
public void Parse(string pathData)
{ {
var span = pathData.AsSpan();
_currentPoint = new Point(); _currentPoint = new Point();
foreach (var commandToken in commandTokens) while(!span.IsEmpty)
{ {
try if(!ReadCommand(ref span, out var command, out var relative))
{
while (true)
{
switch (commandToken.Command)
{
case Command.None:
break;
case Command.FillRule:
SetFillRule(commandToken);
break;
case Command.Move:
AddMove(commandToken);
break;
case Command.Line:
AddLine(commandToken);
break;
case Command.HorizontalLine:
AddHorizontalLine(commandToken);
break;
case Command.VerticalLine:
AddVerticalLine(commandToken);
break;
case Command.CubicBezierCurve:
AddCubicBezierCurve(commandToken);
break;
case Command.QuadraticBezierCurve:
AddQuadraticBezierCurve(commandToken);
break;
case Command.SmoothCubicBezierCurve:
AddSmoothCubicBezierCurve(commandToken);
break;
case Command.SmoothQuadraticBezierCurve:
AddSmoothQuadraticBezierCurve(commandToken);
break;
case Command.Arc:
AddArc(commandToken);
break;
case Command.Close:
CloseFigure();
break;
default:
throw new NotSupportedException("Unsupported command");
}
if (commandToken.HasImplicitCommands)
{
continue;
}
break;
}
}
catch (InvalidDataException)
{ {
break; return;
} }
catch (NotSupportedException)
bool initialCommand = true;
do
{ {
break; if (!initialCommand)
} {
span = ReadSeparator(span);
}
switch (command)
{
case Command.None:
break;
case Command.FillRule:
SetFillRule(ref span);
break;
case Command.Move:
AddMove(ref span, relative);
break;
case Command.Line:
AddLine(ref span, relative);
break;
case Command.HorizontalLine:
AddHorizontalLine(ref span, relative);
break;
case Command.VerticalLine:
AddVerticalLine(ref span, relative);
break;
case Command.CubicBezierCurve:
AddCubicBezierCurve(ref span, relative);
break;
case Command.QuadraticBezierCurve:
AddQuadraticBezierCurve(ref span, relative);
break;
case Command.SmoothCubicBezierCurve:
AddSmoothCubicBezierCurve(ref span, relative);
break;
case Command.SmoothQuadraticBezierCurve:
AddSmoothQuadraticBezierCurve(ref span, relative);
break;
case Command.Arc:
AddArc(ref span, relative);
break;
case Command.Close:
CloseFigure();
break;
default:
throw new NotSupportedException("Unsupported command");
}
initialCommand = false;
} while (PeekArgument(span));
} }
if (_isOpen != null) if (_isOpen)
{ {
_geometryContext.EndFigure(false); _geometryContext.EndFigure(false);
} }
@ -252,7 +173,7 @@ namespace Avalonia.Media
private void CreateFigure() private void CreateFigure()
{ {
if (_isOpen != null) if (_isOpen)
{ {
_geometryContext.EndFigure(false); _geometryContext.EndFigure(false);
} }
@ -262,62 +183,72 @@ namespace Avalonia.Media
_isOpen = true; _isOpen = true;
} }
private void SetFillRule(CommandToken commandToken) private void SetFillRule(ref ReadOnlySpan<char> span)
{ {
var fillRule = commandToken.ReadFillRule(); if (!ReadArgument(ref span, out var fillRule) || fillRule.Length != 1)
{
throw new InvalidDataException("Invalid fill rule.");
}
FillRule rule;
_geometryContext.SetFillRule(fillRule); switch (fillRule[0])
{
case '0':
rule = FillRule.EvenOdd;
break;
case '1':
rule = FillRule.NonZero;
break;
default:
throw new InvalidDataException("Invalid fill rule");
}
_geometryContext.SetFillRule(rule);
} }
private void CloseFigure() private void CloseFigure()
{ {
if (_isOpen == true) if (_isOpen)
{ {
_geometryContext.EndFigure(true); _geometryContext.EndFigure(true);
} }
_previousControlPoint = null; _previousControlPoint = null;
_isOpen = null; _isOpen = false;
} }
private void AddMove(CommandToken commandToken) private void AddMove(ref ReadOnlySpan<char> span, bool relative)
{ {
var currentPoint = commandToken.IsRelative var currentPoint = relative
? commandToken.ReadRelativePoint(_currentPoint) ? ReadRelativePoint(ref span, _currentPoint)
: commandToken.ReadPoint(); : ReadPoint(ref span);
_currentPoint = currentPoint; _currentPoint = currentPoint;
CreateFigure(); CreateFigure();
if (!commandToken.HasImplicitCommands) while (PeekArgument(span))
{ {
return; span = ReadSeparator(span);
} AddLine(ref span, relative);
while (commandToken.HasImplicitCommands) if (!relative)
{
AddLine(commandToken);
if (commandToken.IsRelative)
{ {
continue; _currentPoint = currentPoint;
CreateFigure();
} }
_currentPoint = currentPoint;
CreateFigure();
} }
} }
private void AddLine(CommandToken commandToken) private void AddLine(ref ReadOnlySpan<char> span, bool relative)
{ {
_currentPoint = commandToken.IsRelative _currentPoint = relative
? commandToken.ReadRelativePoint(_currentPoint) ? ReadRelativePoint(ref span, _currentPoint)
: commandToken.ReadPoint(); : ReadPoint(ref span);
if (_isOpen == null) if (!_isOpen)
{ {
CreateFigure(); CreateFigure();
} }
@ -325,13 +256,13 @@ namespace Avalonia.Media
_geometryContext.LineTo(_currentPoint); _geometryContext.LineTo(_currentPoint);
} }
private void AddHorizontalLine(CommandToken commandToken) private void AddHorizontalLine(ref ReadOnlySpan<char> span, bool relative)
{ {
_currentPoint = commandToken.IsRelative _currentPoint = relative
? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y) ? new Point(_currentPoint.X + ReadDouble(ref span), _currentPoint.Y)
: _currentPoint.WithX(commandToken.ReadDouble()); : _currentPoint.WithX(ReadDouble(ref span));
if (_isOpen == null) if (!_isOpen)
{ {
CreateFigure(); CreateFigure();
} }
@ -339,13 +270,13 @@ namespace Avalonia.Media
_geometryContext.LineTo(_currentPoint); _geometryContext.LineTo(_currentPoint);
} }
private void AddVerticalLine(CommandToken commandToken) private void AddVerticalLine(ref ReadOnlySpan<char> span, bool relative)
{ {
_currentPoint = commandToken.IsRelative _currentPoint = relative
? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble()) ? new Point(_currentPoint.X, _currentPoint.Y + ReadDouble(ref span))
: _currentPoint.WithY(commandToken.ReadDouble()); : _currentPoint.WithY(ReadDouble(ref span));
if (_isOpen == null) if (!_isOpen)
{ {
CreateFigure(); CreateFigure();
} }
@ -353,23 +284,27 @@ namespace Avalonia.Media
_geometryContext.LineTo(_currentPoint); _geometryContext.LineTo(_currentPoint);
} }
private void AddCubicBezierCurve(CommandToken commandToken) private void AddCubicBezierCurve(ref ReadOnlySpan<char> span, bool relative)
{ {
var point1 = commandToken.IsRelative var point1 = relative
? commandToken.ReadRelativePoint(_currentPoint) ? ReadRelativePoint(ref span, _currentPoint)
: commandToken.ReadPoint(); : ReadPoint(ref span);
span = ReadSeparator(span);
var point2 = commandToken.IsRelative var point2 = relative
? commandToken.ReadRelativePoint(_currentPoint) ? ReadRelativePoint(ref span, _currentPoint)
: commandToken.ReadPoint(); : ReadPoint(ref span);
_previousControlPoint = point2; _previousControlPoint = point2;
var point3 = commandToken.IsRelative span = ReadSeparator(span);
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint();
if (_isOpen == null) var point3 = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
if (!_isOpen)
{ {
CreateFigure(); CreateFigure();
} }
@ -379,19 +314,21 @@ namespace Avalonia.Media
_currentPoint = point3; _currentPoint = point3;
} }
private void AddQuadraticBezierCurve(CommandToken commandToken) private void AddQuadraticBezierCurve(ref ReadOnlySpan<char> span, bool relative)
{ {
var start = commandToken.IsRelative var start = relative
? commandToken.ReadRelativePoint(_currentPoint) ? ReadRelativePoint(ref span, _currentPoint)
: commandToken.ReadPoint(); : ReadPoint(ref span);
_previousControlPoint = start; _previousControlPoint = start;
var end = commandToken.IsRelative span = ReadSeparator(span);
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint(); var end = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
if (_isOpen == null) if (!_isOpen)
{ {
CreateFigure(); CreateFigure();
} }
@ -401,22 +338,24 @@ namespace Avalonia.Media
_currentPoint = end; _currentPoint = end;
} }
private void AddSmoothCubicBezierCurve(CommandToken commandToken) private void AddSmoothCubicBezierCurve(ref ReadOnlySpan<char> span, bool relative)
{ {
var point2 = commandToken.IsRelative var point2 = relative
? commandToken.ReadRelativePoint(_currentPoint) ? ReadRelativePoint(ref span, _currentPoint)
: commandToken.ReadPoint(); : ReadPoint(ref span);
span = ReadSeparator(span);
var end = commandToken.IsRelative var end = relative
? commandToken.ReadRelativePoint(_currentPoint) ? ReadRelativePoint(ref span, _currentPoint)
: commandToken.ReadPoint(); : ReadPoint(ref span);
if (_previousControlPoint != null) if (_previousControlPoint != null)
{ {
_previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint);
} }
if (_isOpen == null) if (!_isOpen)
{ {
CreateFigure(); CreateFigure();
} }
@ -428,18 +367,18 @@ namespace Avalonia.Media
_currentPoint = end; _currentPoint = end;
} }
private void AddSmoothQuadraticBezierCurve(CommandToken commandToken) private void AddSmoothQuadraticBezierCurve(ref ReadOnlySpan<char> span, bool relative)
{ {
var end = commandToken.IsRelative var end = relative
? commandToken.ReadRelativePoint(_currentPoint) ? ReadRelativePoint(ref span, _currentPoint)
: commandToken.ReadPoint(); : ReadPoint(ref span);
if (_previousControlPoint != null) if (_previousControlPoint != null)
{ {
_previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint);
} }
if (_isOpen == null) if (!_isOpen)
{ {
CreateFigure(); CreateFigure();
} }
@ -449,21 +388,27 @@ namespace Avalonia.Media
_currentPoint = end; _currentPoint = end;
} }
private void AddArc(CommandToken commandToken) private void AddArc(ref ReadOnlySpan<char> span, bool relative)
{ {
var size = commandToken.ReadSize(); var size = ReadSize(ref span);
var rotationAngle = commandToken.ReadDouble(); span = ReadSeparator(span);
var isLargeArc = commandToken.ReadBool(); var rotationAngle = ReadDouble(ref span);
span = ReadSeparator(span);
var isLargeArc = ReadBool(ref span);
var sweepDirection = commandToken.ReadBool() ? SweepDirection.Clockwise : SweepDirection.CounterClockwise; span = ReadSeparator(span);
var end = commandToken.IsRelative var sweepDirection = ReadBool(ref span) ? SweepDirection.Clockwise : SweepDirection.CounterClockwise;
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint(); span = ReadSeparator(span);
if (_isOpen == null) var end = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
if (!_isOpen)
{ {
CreateFigure(); CreateFigure();
} }
@ -475,210 +420,149 @@ namespace Avalonia.Media
_previousControlPoint = null; _previousControlPoint = null;
} }
private class CommandToken private static bool PeekArgument(ReadOnlySpan<char> span)
{ {
private const string ArgumentExpression = @"-?[0-9]*\.?\d+"; span = SkipWhitespace(span);
private CommandToken(Command command, bool isRelative, IEnumerable<string> arguments)
{
Command = command;
IsRelative = isRelative; return !span.IsEmpty && (span[0] == ',' || span[0] == '-' || char.IsDigit(span[0]));
}
Arguments = new List<string>(arguments);
}
public Command Command { get; }
public bool IsRelative { get; }
public bool HasImplicitCommands private static bool ReadArgument(ref ReadOnlySpan<char> remaining, out ReadOnlySpan<char> argument)
{
remaining = SkipWhitespace(remaining);
if (remaining.IsEmpty)
{ {
get argument = ReadOnlySpan<char>.Empty;
{ return false;
if (CurrentPosition == 0 && Arguments.Count > 0)
{
return true;
}
return CurrentPosition < Arguments.Count - 1;
}
}
private int CurrentPosition { get; set; }
private List<string> Arguments { get; }
public static CommandToken Parse(string s)
{
using (var reader = new StringReader(s))
{
var command = Command.None;
var isRelative = false;
if (!ReadCommand(reader, ref command, ref isRelative))
{
throw new InvalidDataException("No path command declared.");
}
var commandArguments = reader.ReadToEnd();
var argumentMatches = Regex.Matches(commandArguments, ArgumentExpression);
var arguments = new List<string>();
foreach (Match match in argumentMatches)
{
arguments.Add(match.Value);
}
return new CommandToken(command, isRelative, arguments);
}
} }
public FillRule ReadFillRule() var valid = false;
int i = 0;
if (remaining[i] == '-')
{ {
if (CurrentPosition == Arguments.Count) i++;
{
throw new InvalidDataException("Invalid fill rule");
}
var value = Arguments[CurrentPosition];
CurrentPosition++;
switch (value)
{
case "0":
{
return FillRule.EvenOdd;
}
case "1":
{
return FillRule.NonZero;
}
default:
throw new InvalidDataException("Invalid fill rule");
}
} }
for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true;
public bool ReadBool() if (i < remaining.Length && remaining[i] == '.')
{ {
if (CurrentPosition == Arguments.Count) valid = false;
{ i++;
throw new InvalidDataException("Invalid boolean value");
}
var value = Arguments[CurrentPosition];
CurrentPosition++;
switch (value)
{
case "1":
{
return true;
}
case "0":
{
return false;
}
default:
throw new InvalidDataException("Invalid boolean value");
}
} }
for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true;
public double ReadDouble() if (i < remaining.Length)
{ {
if (CurrentPosition == Arguments.Count) // scientific notation
if (remaining[i] == 'E' || remaining[i] == 'e')
{ {
throw new InvalidDataException("Invalid double value"); valid = false;
} i++;
if (remaining[i] == '-' || remaining[i] == '+')
var value = Arguments[CurrentPosition]; {
i++;
CurrentPosition++; for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true;
}
return double.Parse(value, CultureInfo.InvariantCulture); }
} }
public Size ReadSize() if (!valid)
{ {
var width = ReadDouble(); argument = ReadOnlySpan<char>.Empty;
return false;
var height = ReadDouble();
return new Size(width, height);
} }
argument = remaining.Slice(0, i);
remaining = remaining.Slice(i);
return true;
}
public Point ReadPoint()
{
var x = ReadDouble();
var y = ReadDouble();
return new Point(x, y);
}
public Point ReadRelativePoint(Point origin) private static ReadOnlySpan<char> ReadSeparator(ReadOnlySpan<char> span)
{
span = SkipWhitespace(span);
if (!span.IsEmpty && span[0] == ',')
{ {
var x = ReadDouble(); span = span.Slice(1);
}
var y = ReadDouble(); return span;
}
return new Point(origin.X + x, origin.Y + y); private static ReadOnlySpan<char> SkipWhitespace(ReadOnlySpan<char> span)
} {
int i = 0;
for (; i < span.Length && char.IsWhiteSpace(span[i]); i++) ;
return span.Slice(i);
}
private static bool ReadCommand(TextReader reader, ref Command command, ref bool relative) private bool ReadBool(ref ReadOnlySpan<char> span)
{
if (!ReadArgument(ref span, out var boolValue) || boolValue.Length != 1)
{ {
ReadWhitespace(reader); throw new InvalidDataException("Invalid bool rule.");
}
var i = reader.Peek();
switch (boolValue[0])
if (i == -1) {
{ case '0':
return false; return false;
} case '1':
return true;
default:
throw new InvalidDataException("Invalid bool rule");
}
}
var c = (char)i; private double ReadDouble(ref ReadOnlySpan<char> span)
{
if (!ReadArgument(ref span, out var doubleValue))
{
throw new InvalidDataException("Invalid double value");
}
if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out var next)) return double.Parse(doubleValue.ToString(), CultureInfo.InvariantCulture);
{ }
throw new InvalidDataException("Unexpected path command '" + c + "'.");
}
command = next; private Size ReadSize(ref ReadOnlySpan<char> span)
{
var width = ReadDouble(ref span);
span = ReadSeparator(span);
var height = ReadDouble(ref span);
return new Size(width, height);
}
relative = char.IsLower(c); private Point ReadPoint(ref ReadOnlySpan<char> span)
{
var x = ReadDouble(ref span);
span = ReadSeparator(span);
var y = ReadDouble(ref span);
return new Point(x, y);
}
reader.Read(); private Point ReadRelativePoint(ref ReadOnlySpan<char> span, Point origin)
{
var x = ReadDouble(ref span);
span = ReadSeparator(span);
var y = ReadDouble(ref span);
return new Point(origin.X + x, origin.Y + y);
}
return true; private bool ReadCommand(ref ReadOnlySpan<char> span, out Command command, out bool relative)
{
span = SkipWhitespace(span);
if (span.IsEmpty)
{
command = default;
relative = false;
return false;
} }
var c = span[0];
private static void ReadWhitespace(TextReader reader) if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out command))
{ {
int i; throw new InvalidDataException("Unexpected path command '" + c + "'.");
while ((i = reader.Peek()) != -1)
{
var c = (char)i;
if (char.IsWhiteSpace(c))
{
reader.Read();
}
else
{
break;
}
}
} }
relative = char.IsLower(c);
span = span.Slice(1);
return true;
} }
} }
} }

67
src/Avalonia.Visuals/VisualTree/VisualLocator.cs

@ -1,9 +1,8 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reflection; using System.Reflection;
using System.Text; using Avalonia.Reactive;
namespace Avalonia.VisualTree namespace Avalonia.VisualTree
{ {
@ -11,36 +10,54 @@ namespace Avalonia.VisualTree
{ {
public static IObservable<IVisual> Track(IVisual relativeTo, int ancestorLevel, Type ancestorType = null) public static IObservable<IVisual> Track(IVisual relativeTo, int ancestorLevel, Type ancestorType = null)
{ {
return TrackAttachmentToTree(relativeTo).Select(isAttachedToTree => return new VisualTracker(relativeTo, ancestorLevel, ancestorType);
}
private class VisualTracker : LightweightObservableBase<IVisual>
{
private readonly IVisual _relativeTo;
private readonly int _ancestorLevel;
private readonly Type _ancestorType;
public VisualTracker(IVisual relativeTo, int ancestorLevel, Type ancestorType)
{
_relativeTo = relativeTo;
_ancestorLevel = ancestorLevel;
_ancestorType = ancestorType;
}
protected override void Initialize()
{
_relativeTo.AttachedToVisualTree += AttachedDetached;
_relativeTo.DetachedFromVisualTree += AttachedDetached;
}
protected override void Deinitialize()
{ {
if (isAttachedToTree) _relativeTo.AttachedToVisualTree -= AttachedDetached;
_relativeTo.DetachedFromVisualTree -= AttachedDetached;
}
protected override void Subscribed(IObserver<IVisual> observer, bool first)
{
observer.OnNext(GetResult());
}
private void AttachedDetached(object sender, VisualTreeAttachmentEventArgs e) => PublishNext(GetResult());
private IVisual GetResult()
{
if (_relativeTo.IsAttachedToVisualTree)
{ {
return relativeTo.GetVisualAncestors() return _relativeTo.GetVisualAncestors()
.Where(x => ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) .Where(x => _ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true)
.ElementAtOrDefault(ancestorLevel); .ElementAtOrDefault(_ancestorLevel);
} }
else else
{ {
return null; return null;
} }
}); }
}
private static IObservable<bool> TrackAttachmentToTree(IVisual relativeTo)
{
var attached = Observable.FromEventPattern<VisualTreeAttachmentEventArgs>(
x => relativeTo.AttachedToVisualTree += x,
x => relativeTo.AttachedToVisualTree -= x)
.Select(x => true)
.StartWith(relativeTo.IsAttachedToVisualTree);
var detached = Observable.FromEventPattern<VisualTreeAttachmentEventArgs>(
x => relativeTo.DetachedFromVisualTree += x,
x => relativeTo.DetachedFromVisualTree -= x)
.Select(x => false);
var attachmentStatus = attached.Merge(detached);
return attachmentStatus;
} }
} }
} }

44
src/Markup/Avalonia.Markup/Data/Binding.cs

@ -5,11 +5,10 @@ using System;
using System.Linq; using System.Linq;
using System.Reactive; using System.Reactive;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Reactive;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Data namespace Avalonia.Data
@ -190,13 +189,10 @@ namespace Avalonia.Data
if (!targetIsDataContext) if (!targetIsDataContext)
{ {
var update = target.GetObservable(StyledElement.DataContextProperty)
.Skip(1)
.Select(_ => Unit.Default);
var result = new ExpressionObserver( var result = new ExpressionObserver(
() => target.GetValue(StyledElement.DataContextProperty), () => target.GetValue(StyledElement.DataContextProperty),
path, path,
update, new UpdateSignal(target, StyledElement.DataContextProperty),
enableDataValidation); enableDataValidation);
return result; return result;
@ -278,14 +274,10 @@ namespace Avalonia.Data
{ {
Contract.Requires<ArgumentNullException>(target != null); Contract.Requires<ArgumentNullException>(target != null);
var update = target.GetObservable(StyledElement.TemplatedParentProperty)
.Skip(1)
.Select(_ => Unit.Default);
var result = new ExpressionObserver( var result = new ExpressionObserver(
() => target.GetValue(StyledElement.TemplatedParentProperty), () => target.GetValue(StyledElement.TemplatedParentProperty),
path, path,
update, new UpdateSignal(target, StyledElement.TemplatedParentProperty),
enableDataValidation); enableDataValidation);
return result; return result;
@ -306,5 +298,35 @@ namespace Avalonia.Data
Observable.Return((object)null); Observable.Return((object)null);
}).Switch(); }).Switch();
} }
private class UpdateSignal : SingleSubscriberObservableBase<Unit>
{
private readonly IAvaloniaObject _target;
private readonly AvaloniaProperty _property;
public UpdateSignal(IAvaloniaObject target, AvaloniaProperty property)
{
_target = target;
_property = property;
}
protected override void Subscribed()
{
_target.PropertyChanged += PropertyChanged;
}
protected override void Unsubscribed()
{
_target.PropertyChanged -= PropertyChanged;
}
private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == _property)
{
PublishNext(Unit.Default);
}
}
}
} }
} }

2
src/OSX/Avalonia.MonoMac/KeyTransform.cs

@ -200,7 +200,7 @@ namespace Avalonia.MonoMac
[kVK_Return] = Key.Return, [kVK_Return] = Key.Return,
[kVK_Tab] = Key.Tab, [kVK_Tab] = Key.Tab,
[kVK_Space] = Key.Space, [kVK_Space] = Key.Space,
[kVK_Delete] = Key.Delete, [kVK_Delete] = Key.Back,
[kVK_Escape] = Key.Escape, [kVK_Escape] = Key.Escape,
[kVK_Command] = Key.LWin, [kVK_Command] = Key.LWin,
[kVK_Shift] = Key.LeftShift, [kVK_Shift] = Key.LeftShift,

3
src/Windows/Avalonia.Win32/ClipboardImpl.cs

@ -57,6 +57,9 @@ namespace Avalonia.Win32
} }
await OpenClipboard(); await OpenClipboard();
UnmanagedMethods.EmptyClipboard();
try try
{ {
var hGlobal = Marshal.StringToHGlobalUni(text); var hGlobal = Marshal.StringToHGlobalUni(text);

15
tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs

@ -337,6 +337,21 @@ namespace Avalonia.Base.UnitTests.Data.Core
GC.KeepAlive(data); GC.KeepAlive(data);
} }
[Fact]
public void Second_Subscription_Should_Fire_Immediately()
{
var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string));
object result = null;
target.Subscribe();
target.Subscribe(x => result = x);
Assert.Equal("foo", result);
GC.KeepAlive(data);
}
private class Class1 : NotifyingBase private class Class1 : NotifyingBase
{ {
private string _stringValue; private string _stringValue;

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

@ -4,7 +4,6 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reactive.Linq;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core.Plugins; using Avalonia.Data.Core.Plugins;
using Xunit; using Xunit;
@ -58,9 +57,9 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins
var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor); var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor);
Assert.Equal(0, data.ErrorsChangedSubscriptionCount); Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
var sub = validator.Subscribe(_ => { }); validator.Subscribe(_ => { });
Assert.Equal(1, data.ErrorsChangedSubscriptionCount); Assert.Equal(1, data.ErrorsChangedSubscriptionCount);
sub.Dispose(); validator.Unsubscribe();
Assert.Equal(0, data.ErrorsChangedSubscriptionCount); Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
} }

20
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@ -315,6 +315,26 @@ namespace Avalonia.Controls.UnitTests
Assert.Same(before, after); Assert.Same(before, after);
} }
[Fact]
public void Should_Clear_Containers_When_ItemsPresenter_Changes()
{
var target = new ItemsControl
{
Items = new[] { "foo", "bar" },
Template = GetTemplate(),
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Assert.Equal(2, target.ItemContainerGenerator.Containers.Count());
target.Template = GetTemplate();
target.ApplyTemplate();
Assert.Empty(target.ItemContainerGenerator.Containers);
}
[Fact] [Fact]
public void Empty_Class_Should_Initially_Be_Applied() public void Empty_Class_Should_Initially_Be_Applied()
{ {

18
tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs

@ -160,6 +160,24 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(target, child.GetLogicalParent()); Assert.Equal(target, child.GetLogicalParent());
} }
[Fact]
public void Changing_Template_Should_Clear_Old_Templated_Childs_Parent()
{
var target = new TemplatedControl
{
Template = new FuncControlTemplate(_ => new Decorator())
};
target.ApplyTemplate();
var child = (Decorator)target.GetVisualChildren().Single();
target.Template = new FuncControlTemplate(_ => new Canvas());
target.ApplyTemplate();
Assert.Null(child.Parent);
}
[Fact] [Fact]
public void Nested_Templated_Control_Should_Not_Have_Template_Applied() public void Nested_Templated_Control_Should_Not_Have_Template_Applied()
{ {

7
tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs

@ -54,15 +54,16 @@ namespace Avalonia.Styling.UnitTests
} }
[Fact] [Fact]
public void Should_Complete_When_Activator_Completes() public void Should_Error_When_Source_Errors()
{ {
var activator = new BehaviorSubject<bool>(false); var activator = new BehaviorSubject<bool>(false);
var source = new BehaviorSubject<object>(1); var source = new BehaviorSubject<object>(1);
var target = new ActivatedObservable(activator, source, string.Empty); var target = new ActivatedObservable(activator, source, string.Empty);
var error = new Exception();
var completed = false; var completed = false;
target.Subscribe(_ => { }, () => completed = true); target.Subscribe(_ => { }, x => completed = true);
activator.OnCompleted(); source.OnError(error);
Assert.True(completed); Assert.True(completed);
} }

10
tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs

@ -17,6 +17,7 @@ namespace Avalonia.Styling.UnitTests
var source = new TestSubject(); var source = new TestSubject();
var target = new ActivatedSubject(activator, source, string.Empty); var target = new ActivatedSubject(activator, source, string.Empty);
target.Subscribe();
target.OnNext("bar"); target.OnNext("bar");
Assert.Equal(AvaloniaProperty.UnsetValue, source.Value); Assert.Equal(AvaloniaProperty.UnsetValue, source.Value);
activator.OnNext(true); activator.OnNext(true);
@ -36,6 +37,7 @@ namespace Avalonia.Styling.UnitTests
var source = new TestSubject(); var source = new TestSubject();
var target = new ActivatedSubject(activator, source, string.Empty); var target = new ActivatedSubject(activator, source, string.Empty);
target.Subscribe();
activator.OnCompleted(); activator.OnCompleted();
Assert.True(source.Completed); Assert.True(source.Completed);
@ -47,10 +49,14 @@ namespace Avalonia.Styling.UnitTests
var activator = new BehaviorSubject<bool>(false); var activator = new BehaviorSubject<bool>(false);
var source = new TestSubject(); var source = new TestSubject();
var target = new ActivatedSubject(activator, source, string.Empty); var target = new ActivatedSubject(activator, source, string.Empty);
var targetError = default(Exception);
var error = new Exception();
activator.OnError(new Exception()); target.Subscribe(_ => { }, e => targetError = e);
activator.OnError(error);
Assert.NotNull(source.Error); Assert.Same(error, source.Error);
Assert.Same(error, targetError);
} }
private class TestSubject : ISubject<object> private class TestSubject : ISubject<object>

14
tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs

@ -40,6 +40,20 @@ namespace Avalonia.Styling.UnitTests
Assert.True(completed); Assert.True(completed);
} }
[Fact]
public void Should_Error_When_Activator_Errors()
{
var activator = new BehaviorSubject<bool>(false);
var target = new ActivatedValue(activator, 1, string.Empty);
var error = new Exception();
var completed = false;
target.Subscribe(_ => { }, x => completed = true);
activator.OnError(error);
Assert.True(completed);
}
[Fact] [Fact]
public void Should_Unsubscribe_From_Activator_When_All_Subscriptions_Disposed() public void Should_Unsubscribe_From_Activator_When_All_Subscriptions_Disposed()
{ {

24
tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs

@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved. // Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Linq; using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -8,6 +9,7 @@ using Moq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Styling; using Avalonia.Styling;
using Xunit; using Xunit;
using System.Collections.Generic;
namespace Avalonia.Styling.UnitTests namespace Avalonia.Styling.UnitTests
{ {
@ -117,6 +119,28 @@ namespace Avalonia.Styling.UnitTests
Assert.False(await activator.Take(1)); Assert.False(await activator.Take(1));
} }
[Fact]
public void Only_Notifies_When_Result_Changes()
{
// Test for #1698
var control = new Control1
{
Classes = new Classes { "foo" },
};
var target = default(Selector).Class("foo");
var activator = target.Match(control).ObservableResult;
var result = new List<bool>();
using (activator.Subscribe(x => result.Add(x)))
{
control.Classes.Add("bar");
control.Classes.Remove("foo");
}
Assert.Equal(new[] { true, false }, result);
}
public class Control1 : TestControlBase public class Control1 : TestControlBase
{ {
} }

28
tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs

@ -7,6 +7,7 @@ using Xunit;
namespace Avalonia.Visuals.UnitTests.Media namespace Avalonia.Visuals.UnitTests.Media
{ {
using System.Globalization;
using System.IO; using System.IO;
public class PathMarkupParserTests public class PathMarkupParserTests
@ -69,7 +70,7 @@ namespace Avalonia.Visuals.UnitTests.Media
using (var context = new PathGeometryContext(pathGeometry)) using (var context = new PathGeometryContext(pathGeometry))
using (var parser = new PathMarkupParser(context)) using (var parser = new PathMarkupParser(context))
{ {
parser.Parse("F 1M0,0"); parser.Parse("F 1M0,0");
Assert.Equal(FillRule.NonZero, pathGeometry.FillRule); Assert.Equal(FillRule.NonZero, pathGeometry.FillRule);
} }
@ -139,9 +140,32 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Equal(new Point(30, 30), lineSegment.Point); Assert.Equal(new Point(30, 30), lineSegment.Point);
} }
} }
[Fact]
public void Parses_Scientific_Notation_Double()
{
var pathGeometry = new PathGeometry();
using (var context = new PathGeometryContext(pathGeometry))
using (var parser = new PathMarkupParser(context))
{
parser.Parse("M -1.01725E-005 -1.01725e-005");
var figure = pathGeometry.Figures[0];
Assert.Equal(
new Point(
double.Parse("-1.01725E-005", NumberStyles.Float, CultureInfo.InvariantCulture),
double.Parse("-1.01725E-005", NumberStyles.Float, CultureInfo.InvariantCulture)),
figure.StartPoint);
}
}
[Theory] [Theory]
[InlineData("F1M9.0771,11C9.1161,10.701,9.1801,10.352,9.3031,10L9.0001,10 9.0001,6.166 3.0001,9.767 3.0001,10 "
+ "9.99999999997669E-05,10 9.99999999997669E-05,0 3.0001,0 3.0001,0.234 9.0001,3.834 9.0001,0 "
+ "12.0001,0 12.0001,8.062C12.1861,8.043 12.3821,8.031 12.5941,8.031 15.3481,8.031 15.7961,9.826 "
+ "15.9201,11L16.0001,16 9.0001,16 9.0001,12.562 9.0001,11z")] // issue #1708
[InlineData(" M0 0")] [InlineData(" M0 0")]
[InlineData("F1 M24,14 A2,2,0,1,1,20,14 A2,2,0,1,1,24,14 z")] // issue #1107 [InlineData("F1 M24,14 A2,2,0,1,1,20,14 A2,2,0,1,1,24,14 z")] // issue #1107
[InlineData("M0 0L10 10z")] [InlineData("M0 0L10 10z")]

Loading…
Cancel
Save