Browse Source

Merge branch 'master' into feature/ApplicationExitMode

Steven Kirk 7 years ago
parent
commit
ceef449a80
63 changed files with 1624 additions and 1110 deletions
  1. 6 2
      .gitignore
  2. 5 0
      .ncrunch/Avalonia.Designer.HostApp.NetFX.v3.ncrunchproject
  3. 5 0
      .ncrunch/BindingDemo.net461.v3.ncrunchproject
  4. 5 0
      .ncrunch/BindingDemo.netcoreapp2.0.v3.ncrunchproject
  5. 5 0
      .ncrunch/Previewer.v3.ncrunchproject
  6. 5 0
      .ncrunch/RemoteDemo.v3.ncrunchproject
  7. 5 0
      .ncrunch/RenderDemo.net461.v3.ncrunchproject
  8. 5 0
      .ncrunch/RenderDemo.netcoreapp2.0.v3.ncrunchproject
  9. 5 0
      .ncrunch/VirtualizationDemo.net461.v3.ncrunchproject
  10. 5 0
      .ncrunch/VirtualizationDemo.netcoreapp2.0.v3.ncrunchproject
  11. 5 0
      build/System.Memory.props
  12. 1 1
      packages.cake
  13. 37 16
      src/Avalonia.Base/AvaloniaObject.cs
  14. 16 65
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  15. 17 42
      src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs
  16. 38 7
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  17. 0 5
      src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs
  18. 58 62
      src/Avalonia.Base/Data/Core/ExpressionNode.cs
  19. 34 54
      src/Avalonia.Base/Data/Core/ExpressionObserver.cs
  20. 9 2
      src/Avalonia.Base/Data/Core/IndexerNode.cs
  21. 5 5
      src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs
  22. 5 5
      src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs
  23. 2 3
      src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs
  24. 12 2
      src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs
  25. 9 10
      src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs
  26. 8 8
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  27. 6 2
      src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs
  28. 38 30
      src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs
  29. 6 5
      src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs
  30. 15 14
      src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
  31. 12 5
      src/Avalonia.Base/Data/Core/StreamNode.cs
  32. 0 42
      src/Avalonia.Base/Reactive/AvaloniaObservable.cs
  33. 46 0
      src/Avalonia.Base/Reactive/AvaloniaPropertyChangedObservable.cs
  34. 52 0
      src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs
  35. 202 0
      src/Avalonia.Base/Reactive/LightweightObservableBase.cs
  36. 76 0
      src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs
  37. 0 85
      src/Avalonia.Base/Reactive/WeakPropertyChangedObservable.cs
  38. 1 0
      src/Avalonia.Controls/ItemsControl.cs
  39. 14 26
      src/Avalonia.Controls/Mixins/ContentControlMixin.cs
  40. 7 4
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  41. 1 0
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  42. 34 5
      src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs
  43. 105 57
      src/Avalonia.Styling/LogicalTree/ControlLocator.cs
  44. 33 33
      src/Avalonia.Styling/Styling/ActivatedObservable.cs
  45. 35 36
      src/Avalonia.Styling/Styling/ActivatedSubject.cs
  46. 86 25
      src/Avalonia.Styling/Styling/ActivatedValue.cs
  47. 2 2
      src/Avalonia.Styling/Styling/StyleActivator.cs
  48. 58 10
      src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs
  49. 1 0
      src/Avalonia.Visuals/Avalonia.Visuals.csproj
  50. 277 393
      src/Avalonia.Visuals/Media/PathMarkupParser.cs
  51. 42 25
      src/Avalonia.Visuals/VisualTree/VisualLocator.cs
  52. 33 11
      src/Markup/Avalonia.Markup/Data/Binding.cs
  53. 1 1
      src/OSX/Avalonia.MonoMac/KeyTransform.cs
  54. 3 0
      src/Windows/Avalonia.Win32/ClipboardImpl.cs
  55. 15 0
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
  56. 2 3
      tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs
  57. 20 0
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  58. 18 0
      tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs
  59. 4 3
      tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs
  60. 8 2
      tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs
  61. 14 0
      tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs
  62. 24 0
      tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs
  63. 26 2
      tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs

+ 6 - 2
.gitignore

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

+ 5 - 0
.ncrunch/Avalonia.Designer.HostApp.NetFX.v3.ncrunchproject

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

+ 5 - 0
.ncrunch/BindingDemo.net461.v3.ncrunchproject

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

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

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

+ 5 - 0
.ncrunch/Previewer.v3.ncrunchproject

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

+ 5 - 0
.ncrunch/RemoteDemo.v3.ncrunchproject

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

+ 5 - 0
.ncrunch/RenderDemo.net461.v3.ncrunchproject

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

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

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

+ 5 - 0
.ncrunch/VirtualizationDemo.net461.v3.ncrunchproject

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

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

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

+ 5 - 0
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>

+ 1 - 1
packages.cake

@@ -253,7 +253,7 @@ public class Packages
                 }
                 .Deps(new string[]{null, "netcoreapp2.0"},
                     "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(),
                 Files = coreLibrariesNuSpecContent
                     .Concat(win32CoreLibrariesNuSpecContent).Concat(net45RuntimePlatform)

+ 37 - 16
src/Avalonia.Base/AvaloniaObject.cs

@@ -10,6 +10,7 @@ using System.Reactive.Linq;
 using Avalonia.Data;
 using Avalonia.Diagnostics;
 using Avalonia.Logging;
+using Avalonia.Reactive;
 using Avalonia.Threading;
 using Avalonia.Utilities;
 
@@ -38,7 +39,7 @@ namespace Avalonia
         /// Maintains a list of direct property binding subscriptions so that the binding source
         /// doesn't get collected.
         /// </summary>
-        private List<IDisposable> _directBindings;
+        private List<DirectBindingSubscription> _directBindings;
 
         /// <summary>
         /// Event handler for <see cref="INotifyPropertyChanged"/> implementation.
@@ -359,25 +360,12 @@ namespace Avalonia
                     property, 
                     description);
 
-                IDisposable subscription = null;
-
                 if (_directBindings == null)
                 {
-                    _directBindings = new List<IDisposable>();
+                    _directBindings = new List<DirectBindingSubscription>();
                 }
 
-                subscription = 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);
-                });
+                return new DirectBindingSubscription(this, property, source);
             }
             else
             {
@@ -908,5 +896,38 @@ namespace Avalonia
                 value,
                 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);
+            }
+        }
     }
 }

+ 16 - 65
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
         /// object and subsequently each time the property value changes.
         /// </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)
         {
             Contract.Requires<ArgumentNullException>(o != null);
             Contract.Requires<ArgumentNullException>(property != null);
 
-            return new AvaloniaObservable<object>(
-                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));
+            return new AvaloniaPropertyObservable<object>(o, property);
         }
 
         /// <summary>
@@ -74,51 +57,36 @@ namespace Avalonia
         /// An observable which fires immediately with the current value of the property on the
         /// object and subsequently each time the property value changes.
         /// </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)
         {
             Contract.Requires<ArgumentNullException>(o != null);
             Contract.Requires<ArgumentNullException>(property != null);
 
-            return o.GetObservable((AvaloniaProperty)property).Cast<T>();
+            return new AvaloniaPropertyObservable<T>(o, property);
         }
 
         /// <summary>
-        /// Gets an observable for a <see cref="AvaloniaProperty"/>.
+        /// Gets an observable that listens for property changed events for an
+        /// <see cref="AvaloniaProperty"/>.
         /// </summary>
         /// <param name="o">The object.</param>
-        /// <typeparam name="T">The type of the property.</typeparam>
         /// <param name="property">The property.</param>
         /// <returns>
-        /// An observable which when subscribed pushes the old and new values of the property each
-        /// time it is changed. Note that the observable returned from this method does not fire
-        /// with the current value of the property immediately.
+        /// An observable which when subscribed pushes the property changed event args
+        /// each time a <see cref="IAvaloniaObject.PropertyChanged"/> event is raised
+        /// for the specified property.
         /// </returns>
-        public static IObservable<Tuple<T, T>> GetObservableWithHistory<T>(
+        public static IObservable<AvaloniaPropertyChangedEventArgs> GetPropertyChangedObservable(
             this IAvaloniaObject o, 
-            AvaloniaProperty<T> property)
+            AvaloniaProperty property)
         {
             Contract.Requires<ArgumentNullException>(o != null);
             Contract.Requires<ArgumentNullException>(property != null);
 
-            return new AvaloniaObservable<Tuple<T, T>>(
-                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));
+            return new AvaloniaPropertyChangedObservable(o, property);
         }
 
         /// <summary>
@@ -166,23 +134,6 @@ namespace Avalonia
                 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>
         /// Binds a property on an <see cref="IAvaloniaObject"/> to an <see cref="IBinding"/>.
         /// </summary>

+ 17 - 42
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.
 
 using System;
-using System.Collections;
-using System.Collections.Generic;
 using System.Collections.Specialized;
-using System.Reactive;
-using System.Reactive.Disposables;
 using System.Reactive.Linq;
-using System.Reactive.Subjects;
+using Avalonia.Reactive;
 using Avalonia.Utilities;
 
 namespace Avalonia.Collections
@@ -43,9 +39,8 @@ namespace Avalonia.Collections
             Contract.Requires<ArgumentNullException>(collection != null);
             Contract.Requires<ArgumentNullException>(handler != null);
 
-            return
-                collection.GetWeakCollectionChangedObservable()
-                          .Subscribe(e => handler.Invoke(collection, e));
+            return collection.GetWeakCollectionChangedObservable()
+                .Subscribe(e => handler(collection, e));
         }
 
         /// <summary>
@@ -63,18 +58,13 @@ namespace Avalonia.Collections
             Contract.Requires<ArgumentNullException>(collection != null);
             Contract.Requires<ArgumentNullException>(handler != null);
 
-            return
-                collection.GetWeakCollectionChangedObservable()
-                          .Subscribe(handler);
+            return collection.GetWeakCollectionChangedObservable().Subscribe(handler);
         }
 
-        private class WeakCollectionChangedObservable : ObservableBase<NotifyCollectionChangedEventArgs>,
+        private class WeakCollectionChangedObservable : LightweightObservableBase<NotifyCollectionChangedEventArgs>,
             IWeakSubscriber<NotifyCollectionChangedEventArgs>
         {
             private WeakReference<INotifyCollectionChanged> _sourceReference;
-            private readonly Subject<NotifyCollectionChangedEventArgs> _changed = new Subject<NotifyCollectionChangedEventArgs>();
-
-            private int _count;
 
             public WeakCollectionChangedObservable(WeakReference<INotifyCollectionChanged> source)
             {
@@ -83,43 +73,28 @@ namespace Avalonia.Collections
 
             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 (_count++ == 0)
-                    {
-                        WeakSubscriptionManager.Subscribe(
-                            instance,
-                            nameof(instance.CollectionChanged),
-                            this);
-                    }
-
-                    return Observable.Using(() => Disposable.Create(DecrementCount), _ => _changed)
-                        .Subscribe(observer);
-                }
-                else
-                {
-                    _changed.OnCompleted();
-                    observer.OnCompleted();
-                    return Disposable.Empty;
+                    WeakSubscriptionManager.Subscribe(
+                    instance,
+                    nameof(instance.CollectionChanged),
+                    this);
                 }
             }
 
-            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,
-                            nameof(instance.CollectionChanged),
-                            this);
-                    }
+                    WeakSubscriptionManager.Unsubscribe(
+                        instance,
+                        nameof(instance.CollectionChanged),
+                        this);
                 }
             }
         }

+ 38 - 7
src/Avalonia.Base/Data/Core/BindingExpression.cs

@@ -7,21 +7,23 @@ using System.Reactive.Linq;
 using System.Reactive.Subjects;
 using Avalonia.Data.Converters;
 using Avalonia.Logging;
+using Avalonia.Reactive;
 using Avalonia.Utilities;
 
 namespace Avalonia.Data.Core
 {
     /// <summary>
     /// 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>
-    public class BindingExpression : ISubject<object>, IDescription
+    public class BindingExpression : LightweightObservableBase<object>, ISubject<object>, IDescription
     {
         private readonly ExpressionObserver _inner;
         private readonly Type _targetType;
         private readonly object _fallbackValue;
         private readonly BindingPriority _priority;
-        private readonly Subject<object> _errors = new Subject<object>();
+        InnerListener _innerListener;
+        WeakReference<object> _value;
 
         /// <summary>
         /// 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.");
                         }
 
-                        _errors.OnNext(notification);
+                        PublishNext(notification);
 
                         if (_fallbackValue != AvaloniaProperty.UnsetValue)
                         {
@@ -170,12 +172,18 @@ namespace Avalonia.Data.Core
             }
         }
 
-        /// <inheritdoc/>
-        public IDisposable Subscribe(IObserver<object> observer)
+        protected override void Initialize() => _innerListener = new InnerListener(this);
+        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)
         {
             var notification = value as BindingNotification;
@@ -301,5 +309,28 @@ namespace Avalonia.Data.Core
 
             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);
+            }
+        }
     }
 }

+ 0 - 5
src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs

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

+ 58 - 62
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.
 
 using System;
-using System.Reactive.Disposables;
-using System.Reactive.Linq;
-using System.Reactive.Subjects;
-using Avalonia.Data;
 
 namespace Avalonia.Data.Core
 {
-    internal abstract class ExpressionNode : ISubject<object>
+    internal abstract class ExpressionNode
     {
         private static readonly object CacheInvalid = new object();
         protected static readonly WeakReference UnsetReference = 
             new WeakReference(AvaloniaProperty.UnsetValue);
 
         private WeakReference _target = UnsetReference;
-        private IDisposable _valueSubscription;
-        private IObserver<object> _observer;
+        private Action<object> _subscriber;
+        private bool _listening;
 
         protected WeakReference LastValue { get; private set; }
 
@@ -33,92 +29,66 @@ namespace Avalonia.Data.Core
 
                 var oldTarget = _target?.Target;
                 var newTarget = value.Target;
-                var running = _valueSubscription != null;
 
                 if (!ReferenceEquals(oldTarget, newTarget))
                 {
-                    _valueSubscription?.Dispose();
-                    _valueSubscription = null;
+                    if (_listening)
+                    {
+                        StopListening();
+                    }
+
                     _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.");
             }
 
-            _observer = observer;
-            var nextSubscription = Next?.Subscribe(this);
-            _valueSubscription = StartListening();
-
-            return Disposable.Create(() =>
-            {
-                _valueSubscription?.Dispose();
-                _valueSubscription = null;
-                LastValue = null;
-                nextSubscription?.Dispose();
-                _observer = null;
-            });
+            _subscriber = subscriber;
+            Next?.Subscribe(NextValueChanged);
+            StartListening();
         }
 
-        void IObserver<object>.OnCompleted()
+        public void Unsubscribe()
         {
-            throw new AvaloniaInternalException("ExpressionNode.OnCompleted should not be called.");
-        }
+            Next?.Unsubscribe();
 
-        void IObserver<object>.OnError(Exception error)
-        {
-            throw new AvaloniaInternalException("ExpressionNode.OnError should not be called.");
+            if (_listening)
+            {
+                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)
         {
             var bindingBroken = BindingNotification.ExtractError(value) as MarkupBindingChainException;
             bindingBroken?.AddNode(Description);
-            _observer.OnNext(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);
+            _subscriber(value);
         }
 
-        private void ValueChanged(object value)
+        protected void ValueChanged(object value)
         {
             var notification = value as BindingNotification;
 
@@ -131,24 +101,50 @@ namespace Avalonia.Data.Core
                 }
                 else
                 {
-                    _observer.OnNext(value);
+                    _subscriber(value);
                 }
             }
             else
             {
                 LastValue = new WeakReference(notification.Value);
+
                 if (Next != null)
                 {
                     Next.Target = new WeakReference(notification.Value);
                 }
-                
+
                 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()
         {
             return new BindingNotification(

+ 34 - 54
src/Avalonia.Base/Data/Core/ExpressionObserver.cs

@@ -4,18 +4,17 @@
 using System;
 using System.Collections.Generic;
 using System.Reactive;
-using System.Reactive.Disposables;
 using System.Reactive.Linq;
-using System.Reactive.Subjects;
 using Avalonia.Data;
 using Avalonia.Data.Core.Plugins;
+using Avalonia.Reactive;
 
 namespace Avalonia.Data.Core
 {
     /// <summary>
     /// Observes and sets the value of an expression on an object.
     /// </summary>
-    public class ExpressionObserver : ObservableBase<object>, IDescription
+    public class ExpressionObserver : LightweightObservableBase<object>, IDescription
     {
         /// <summary>
         /// 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 readonly ExpressionNode _node;
-        private readonly Subject<Unit> _finished;
-        private readonly object _root;
-        private IObservable<object> _result;
+        private object _root;
+        private IDisposable _rootSubscription;
+        private WeakReference<object> _value;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
@@ -107,7 +106,6 @@ namespace Avalonia.Data.Core
             Expression = expression;
             Description = description ?? expression;
             _node = Parse(expression, enableDataValidation);
-            _finished = new Subject<Unit>();
             _root = rootObservable;
         }
 
@@ -135,8 +133,6 @@ namespace Avalonia.Data.Core
             Expression = expression;
             Description = description ?? expression;
             _node = Parse(expression, enableDataValidation);
-            _finished = new Subject<Unit>();
-
             _node.Target = new WeakReference(rootGetter());
             _root = update.Select(x => rootGetter());
         }
@@ -203,27 +199,26 @@ namespace Avalonia.Data.Core
             }
         }
 
-        /// <inheritdoc/>
-        protected override IDisposable SubscribeCore(IObserver<object> observer)
+        protected override void Initialize()
         {
-            if (_result == null)
-            {
-                var source = (IObservable<object>)_node;
+            _value = null;
+            _node.Subscribe(ValueChanged);
+            StartRoot();
+        }
 
-                if (_finished != null)
-                {
-                    source = source.TakeUntil(_finished);
-                }
+        protected override void Deinitialize()
+        {
+            _rootSubscription?.Dispose();
+            _rootSubscription = null;
+            _node.Unsubscribe();
+        }
 
-                _result = Observable.Using(StartRoot, _ => source)
-                    .Select(ToWeakReference)
-                    .Publish(UninitializedValue)
-                    .RefCount()
-                    .Where(x => x != UninitializedValue)
-                    .Select(Translate);
+        protected override void Subscribed(IObserver<object> observer, bool first)
+        {
+            if (!first && _value != null && _value.TryGetTarget(out var value))
+            {
+                observer.OnNext(value);
             }
-
-            return _result.Subscribe(observer);
         }
 
         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);
-        }
-
-        private object Translate(object o)
-        {
-            if (o is WeakReference weak)
+            if (_root is IObservable<object> observable)
             {
-                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)
-            {
-                case IObservable<object> observable:
-                    return observable.Subscribe(
-                        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;
+            var broken = BindingNotification.ExtractError(value) as MarkupBindingChainException;
+            broken?.Commit(Description);
+            _value = new WeakReference<object>(value);
+            PublishNext(value);
         }
     }
 }

+ 9 - 2
src/Avalonia.Base/Data/Core/IndexerNode.cs

@@ -17,6 +17,8 @@ namespace Avalonia.Data.Core
 {
     internal class IndexerNode :  SettableNode
     {
+        private IDisposable _subscription;
+
         public IndexerNode(IList<string> arguments)
         {
             Arguments = arguments;
@@ -24,7 +26,7 @@ namespace Avalonia.Data.Core
 
         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 incc = target as INotifyCollectionChanged;
@@ -49,7 +51,12 @@ namespace Avalonia.Data.Core
                     .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)

+ 5 - 5
src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs

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

+ 5 - 5
src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs

@@ -55,13 +55,13 @@ namespace Avalonia.Data.Core.Plugins
         /// <param name="value">The value.</param>
         void IObserver<object>.OnNext(object value) => InnerValueChanged(value);
 
-        /// <inheritdoc/>
-        protected override void Dispose(bool disposing) => _inner.Dispose();
-
         /// <summary>
         /// Begins listening to the inner <see cref="IPropertyAccessor"/>.
         /// </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>
         /// 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)
         {
             var notification = value as BindingNotification ?? new BindingNotification(value);
-            Observer.OnNext(notification);
+            PublishValue(notification);
         }
     }
 }

+ 2 - 3
src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs

@@ -1,7 +1,6 @@
 // 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 Avalonia.Data;
 using System;
 using System.Reflection;
 
@@ -36,11 +35,11 @@ namespace Avalonia.Data.Core.Plugins
                 }
                 catch (TargetInvocationException ex)
                 {
-                    Observer.OnNext(new BindingNotification(ex.InnerException, BindingErrorType.DataValidationError));
+                    PublishValue(new BindingNotification(ex.InnerException, BindingErrorType.DataValidationError));
                 }
                 catch (Exception ex)
                 {
-                    Observer.OnNext(new BindingNotification(ex, BindingErrorType.DataValidationError));
+                    PublishValue(new BindingNotification(ex, BindingErrorType.DataValidationError));
                 }
 
                 return false;

+ 12 - 2
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.
 
 using System;
-using Avalonia.Data;
 
 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 
     /// <see cref="IPropertyAccessorPlugin"/>
     /// </summary>
-    public interface IPropertyAccessor : IObservable<object>, IDisposable
+    public interface IPropertyAccessor : IDisposable
     {
         /// <summary>
         /// 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.
         /// </returns>
         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();
     }
 }

+ 9 - 10
src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs

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

+ 8 - 8
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;
 
@@ -116,18 +122,12 @@ namespace Avalonia.Data.Core.Plugins
                 }
             }
 
-            protected override void SubscribeCore(IObserver<object> observer)
-            {
-                SendCurrentValue();
-                SubscribeToChanges();
-            }
-
             private void SendCurrentValue()
             {
                 try
                 {
                     var value = Value;
-                    Observer.OnNext(value);
+                    PublishValue(value);
                 }
                 catch { }
             }

+ 6 - 2
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;
 
-            protected override void SubscribeCore(IObserver<object> observer)
+            protected override void SubscribeCore()
             {
                 try
                 {
-                    Observer.OnNext(Value);
+                    PublishValue(Value);
                 }
                 catch { }
             }
+
+            protected override void UnsubscribeCore()
+            {
+            }
         }
     }
 }

+ 38 - 30
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.
 
 using System;
-using Avalonia.Data;
 
 namespace Avalonia.Data.Core.Plugins
 {
     /// <summary>
     /// Defines a default base implementation for a <see cref="IPropertyAccessor"/>.
     /// </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
     {
+        private Action<object> _listener;
+
         /// <inheritdoc/>
         public abstract Type PropertyType { get; }
 
         /// <inheritdoc/>
         public abstract object Value { get; }
 
-        /// <summary>
-        /// Stops the subscription.
-        /// </summary>
-        public void Dispose() => Dispose(true);
+        /// <inheritdoc/>
+        public void Dispose()
+        {
+            if (_listener != null)
+            {
+                Unsubscribe();
+            }
+        }
 
         /// <inheritdoc/>
         public abstract bool SetValue(object value, BindingPriority priority);
 
-        /// <summary>
-        /// The currently subscribed observer.
-        /// </summary>
-        protected IObserver<object> Observer { get; private set; }
-
         /// <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(
-                    "A property accessor can be subscribed to only once.");
+                    "A member accessor can be subscribed to only once.");
             }
 
-            Observer = observer;
-            SubscribeCore(observer);
-            return this;
+            _listener = listener;
+            SubscribeCore();
         }
 
+        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>
-        /// Stops listening to the property.
+        /// When overridden in a derived class, begins listening to the member.
         /// </summary>
-        /// <param name="disposing">
-        /// 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;
+        protected abstract void SubscribeCore();
 
         /// <summary>
-        /// When overridden in a derived class, begins listening to the property.
+        /// When overridden in a derived class, stops listening to the member.
         /// </summary>
-        protected abstract void SubscribeCore(IObserver<object> observer);
+        protected abstract void UnsubscribeCore();
     }
 }

+ 6 - 5
src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs

@@ -1,6 +1,4 @@
 using System;
-using System.Reactive.Disposables;
-using Avalonia.Data;
 
 namespace Avalonia.Data.Core.Plugins
 {
@@ -37,10 +35,13 @@ namespace Avalonia.Data.Core.Plugins
             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;
         }
     }
 }

+ 15 - 14
src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs

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

+ 12 - 5
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.
 
 using System;
-using System.Globalization;
-using Avalonia.Data;
 using System.Reactive.Linq;
 
 namespace Avalonia.Data.Core
 {
     internal class StreamNode : ExpressionNode
     {
+        private IDisposable _subscription;
+
         public override string Description => "^";
 
-        protected override IObservable<object> StartListeningCore(WeakReference reference)
+        protected override void StartListeningCore(WeakReference reference)
         {
             foreach (var plugin in ExpressionObserver.StreamHandlers)
             {
                 if (plugin.Match(reference))
                 {
-                    return plugin.Start(reference);
+                    _subscription = plugin.Start(reference).Subscribe(ValueChanged);
+                    return;
                 }
             }
 
             // TODO: Improve error.
-            return Observable.Return(new BindingNotification(
+            ValueChanged(new BindingNotification(
                 new MarkupBindingChainException("Stream operator applied to unsupported type", Description),
                 BindingErrorType.Error));
         }
+
+        protected override void StopListeningCore()
+        {
+            _subscription?.Dispose();
+            _subscription = null;
+        }
     }
 }

+ 0 - 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 - 0
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 - 0
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 - 0
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 - 0
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();
+    }
+}

+ 0 - 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 - 0
src/Avalonia.Controls/ItemsControl.cs

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

+ 14 - 26
src/Avalonia.Controls/Mixins/ContentControlMixin.cs

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

+ 7 - 4
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@@ -408,12 +408,15 @@ namespace Avalonia.Controls.Primitives
 
             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);
-                    break;
+                    if (KeyboardNavigation.GetTabOnceActiveElement(panel) == container.ContainerControl)
+                    {
+                        KeyboardNavigation.SetTabOnceActiveElement(panel, null);
+                        break;
+                    }
                 }
             }
         }

+ 1 - 0
src/Avalonia.Controls/Primitives/TemplatedControl.cs

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

+ 34 - 5
src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Reactive;
 using System.Reactive.Linq;
+using Avalonia.Reactive;
 
 namespace Avalonia.Controls
 {
@@ -55,11 +56,39 @@ namespace Avalonia.Controls
 
         public static IObservable<object> GetResourceObservable(this IResourceNode target, string key)
         {
-            return Observable.FromEventPattern<ResourcesChangedEventArgs>(
-                x => target.ResourcesChanged += x,
-                x => target.ResourcesChanged -= x)
-                .StartWith((EventPattern<ResourcesChangedEventArgs>)null)
-                .Select(x => target.FindResource(key));
+            return new ResourceObservable(target, key);
+        }
+
+        private class ResourceObservable : LightweightObservableBase<object>
+        {
+            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));
+            }
         }
     }
 }

+ 105 - 57
src/Avalonia.Styling/LogicalTree/ControlLocator.cs

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

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

+ 35 - 36
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.
 
 using System;
-using System.Reactive.Linq;
 using System.Reactive.Subjects;
 
 namespace Avalonia.Styling
@@ -11,17 +10,14 @@ namespace Avalonia.Styling
     /// A subject which is switched on or off according to an activator observable.
     /// </summary>
     /// <remarks>
-    /// An <see cref="ActivatedSubject"/> has two inputs: an activator observable and either an
-    /// <see cref="ActivatedValue"/> or a <see cref="Source"/> observable which produces the
-    /// activated value. When the activator produces true, the <see cref="ActivatedObservable"/> will
-    /// produce the current activated value. When the activator produces false it will produce
-    /// <see cref="AvaloniaProperty.UnsetValue"/>.
+    /// An <see cref="ActivatedSubject"/> extends <see cref="ActivatedObservable"/> to
+    /// be an <see cref="ISubject{Object}"/>. When the object is active then values
+    /// received via <see cref="OnNext(object)"/> will be passed to the source subject.
     /// </remarks>
     internal class ActivatedSubject : ActivatedObservable, ISubject<object>, IDescription
     {
-        private bool? _active;
         private bool _completed;
-        private object _value;
+        private object _pushValue;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ActivatedSubject"/> class.
@@ -35,7 +31,6 @@ namespace Avalonia.Styling
             string description)
             : base(activator, source, description)
         {
-            Activator.Subscribe(ActivatorChanged, ActivatorError, ActivatorCompleted);
         }
 
         /// <summary>
@@ -46,53 +41,57 @@ namespace Avalonia.Styling
             get { return (ISubject<object>)base.Source; }
         }
 
-        /// <summary>
-        /// Notifies all subscribed observers about the end of the sequence.
-        /// </summary>
         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)
         {
-            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)
         {
-            _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)
             {
-                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;
             }
         }
 

+ 86 - 25
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.
 
 using System;
-using System.Reactive;
-using System.Reactive.Linq;
+using Avalonia.Reactive;
 
 namespace Avalonia.Styling
 {
@@ -16,12 +15,12 @@ namespace Avalonia.Styling
     /// <see cref="ActivatedValue"/> will produce the current value. When the activator 
     /// produces false it will produce <see cref="AvaloniaProperty.UnsetValue"/>.
     /// </remarks>
-    internal class ActivatedValue : ObservableBase<object>, IDescription
+    internal class ActivatedValue : LightweightObservableBase<object>, IDescription
     {
-        /// <summary>
-        /// The activator.
-        /// </summary>
-        private readonly IObservable<bool> _activator;
+        private static readonly object NotSent = new object();
+        private IDisposable _activatorSubscription;
+        private object _value;
+        private object _last = NotSent;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ActivatedObservable"/> class.
@@ -34,39 +33,101 @@ namespace Avalonia.Styling
             object value,
             string description)
         {
-            _activator = activator;
+            Contract.Requires<ArgumentNullException>(activator != null);
+
+            Activator = activator;
             Value = value;
             Description = description;
+            Listener = CreateListener();
         }
 
         /// <summary>
-        /// Gets the activated value.
+        /// Gets the activator observable.
         /// </summary>
-        public object Value
-        {
-            get;
-        }
+        public IObservable<bool> Activator { get; }
 
         /// <summary>
         /// Gets a description of the binding.
         /// </summary>
-        public string Description
-        {
-            get;
-        }
+        public string Description { get; }
+
+        /// <summary>
+        /// Gets a value indicating whether the activator is active.
+        /// </summary>
+        public bool? IsActive { get; private set; }
 
         /// <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>
-        /// <param name="observer">The observer.</param>
-        /// <returns>IDisposable object used to unsubscribe from the observable sequence.</returns>
-        protected override IDisposable SubscribeCore(IObserver<object> observer)
+        public object Value
+        {
+            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
-                .Select(active => active ? Value : AvaloniaProperty.UnsetValue)
-                .Subscribe(observer);
+            void IObserver<bool>.OnCompleted() => Parent.CompletedReceived();
+            void IObserver<bool>.OnError(Exception error) => Parent.ErrorReceived(error);
+            void IObserver<bool>.OnNext(bool value) => Parent.ActiveChanged(value);
         }
     }
 }

+ 2 - 2
src/Avalonia.Styling/Styling/StyleActivator.cs

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

+ 58 - 10
src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs

@@ -4,10 +4,10 @@
 using System;
 using System.Collections.Generic;
 using System.Collections.Specialized;
-using System.Reactive;
-using System.Reactive.Linq;
 using System.Reflection;
 using System.Text;
+using Avalonia.Collections;
+using Avalonia.Reactive;
 
 namespace Avalonia.Styling
 {
@@ -122,14 +122,7 @@ namespace Avalonia.Styling
             {
                 if (subscribe)
                 {
-                    var observable = Observable.FromEventPattern<
-                            NotifyCollectionChangedEventHandler,
-                            NotifyCollectionChangedEventArgs>(
-                        x => control.Classes.CollectionChanged += x,
-                        x => control.Classes.CollectionChanged -= x)
-                        .StartWith((EventPattern<NotifyCollectionChangedEventArgs>)null)
-                        .Select(_ => Matches(control.Classes))
-                        .DistinctUntilChanged();
+                    var observable = new ClassObserver(control.Classes, _classes.Value);
                     return new SelectorMatch(observable);
                 }
                 else
@@ -204,5 +197,60 @@ namespace Avalonia.Styling
 
             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 - 0
src/Avalonia.Visuals/Avalonia.Visuals.csproj

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

+ 277 - 393
src/Avalonia.Visuals/Media/PathMarkupParser.cs

@@ -5,9 +5,6 @@ using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
-using System.Linq;
-using System.Text;
-using System.Text.RegularExpressions;
 using Avalonia.Platform;
 
 namespace Avalonia.Media
@@ -17,7 +14,6 @@ namespace Avalonia.Media
     /// </summary>
     public class PathMarkupParser : IDisposable
     {
-        private static readonly string s_separatorPattern;
         private static readonly Dictionary<char, Command> s_commands =
             new Dictionary<char, Command>
                 {
@@ -37,14 +33,9 @@ namespace Avalonia.Media
         private IGeometryContext _geometryContext;
         private Point _currentPoint;
         private Point? _previousControlPoint;
-        private bool? _isOpen;
+        private bool _isOpen;
         private bool _isDisposed;
 
-        static PathMarkupParser()
-        {
-            s_separatorPattern = CreatesSeparatorPattern();
-        }
-
         /// <summary>
         /// Initializes a new instance of the <see cref="PathMarkupParser"/> class.
         /// </summary>
@@ -76,18 +67,6 @@ namespace Avalonia.Media
             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()
         {
             Dispose(true);
@@ -108,66 +87,6 @@ namespace Avalonia.Media
             _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)
         {
             var dir = controlPoint - center;
@@ -175,76 +94,78 @@ namespace Avalonia.Media
             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();
 
-            foreach (var commandToken in commandTokens)
+            while(!span.IsEmpty)
             {
-                try
-                {
-                    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)
+                if(!ReadCommand(ref span, out var command, out var relative))
                 {
-                    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);
             }
@@ -252,7 +173,7 @@ namespace Avalonia.Media
 
         private void CreateFigure()
         {
-            if (_isOpen != null)
+            if (_isOpen)
             {
                 _geometryContext.EndFigure(false);
             }
@@ -262,62 +183,72 @@ namespace Avalonia.Media
             _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()
         {
-            if (_isOpen == true)
+            if (_isOpen)
             {
                 _geometryContext.EndFigure(true);
             }
 
             _previousControlPoint = null;
 
-            _isOpen = null;
+            _isOpen = false;
         }
 
-        private void AddMove(CommandToken commandToken)
+        private void AddMove(ref ReadOnlySpan<char> span, bool relative)
         {
-            var currentPoint = commandToken.IsRelative
-                                   ? commandToken.ReadRelativePoint(_currentPoint)
-                                   : commandToken.ReadPoint();
+            var currentPoint = relative
+                                ? ReadRelativePoint(ref span, _currentPoint)
+                                : ReadPoint(ref span);
 
             _currentPoint = currentPoint;
 
             CreateFigure();
 
-            if (!commandToken.HasImplicitCommands)
+            while (PeekArgument(span))
             {
-                return;
-            }
+                span = ReadSeparator(span);
+                AddLine(ref span, relative);
 
-            while (commandToken.HasImplicitCommands)
-            {
-                AddLine(commandToken);
-
-                if (commandToken.IsRelative)
+                if (!relative)
                 {
-                    continue;
+                    _currentPoint = currentPoint;
+                    CreateFigure();
                 }
-
-                _currentPoint = currentPoint;
-
-                CreateFigure();
             }
         }
 
-        private void AddLine(CommandToken commandToken)
+        private void AddLine(ref ReadOnlySpan<char> span, bool relative)
         {
-            _currentPoint = commandToken.IsRelative
-                                ? commandToken.ReadRelativePoint(_currentPoint)
-                                : commandToken.ReadPoint();
+            _currentPoint = relative
+                                ? ReadRelativePoint(ref span, _currentPoint)
+                                : ReadPoint(ref span);
 
-            if (_isOpen == null)
+            if (!_isOpen)
             {
                 CreateFigure();
             }
@@ -325,13 +256,13 @@ namespace Avalonia.Media
             _geometryContext.LineTo(_currentPoint);
         }
 
-        private void AddHorizontalLine(CommandToken commandToken)
+        private void AddHorizontalLine(ref ReadOnlySpan<char> span, bool relative)
         {
-            _currentPoint = commandToken.IsRelative
-                                ? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y)
-                                : _currentPoint.WithX(commandToken.ReadDouble());
+            _currentPoint = relative
+                                ? new Point(_currentPoint.X + ReadDouble(ref span), _currentPoint.Y)
+                                : _currentPoint.WithX(ReadDouble(ref span));
 
-            if (_isOpen == null)
+            if (!_isOpen)
             {
                 CreateFigure();
             }
@@ -339,13 +270,13 @@ namespace Avalonia.Media
             _geometryContext.LineTo(_currentPoint);
         }
 
-        private void AddVerticalLine(CommandToken commandToken)
+        private void AddVerticalLine(ref ReadOnlySpan<char> span, bool relative)
         {
-            _currentPoint = commandToken.IsRelative
-                                ? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble())
-                                : _currentPoint.WithY(commandToken.ReadDouble());
+            _currentPoint = relative
+                                ? new Point(_currentPoint.X, _currentPoint.Y + ReadDouble(ref span))
+                                : _currentPoint.WithY(ReadDouble(ref span));
 
-            if (_isOpen == null)
+            if (!_isOpen)
             {
                 CreateFigure();
             }
@@ -353,23 +284,27 @@ namespace Avalonia.Media
             _geometryContext.LineTo(_currentPoint);
         }
 
-        private void AddCubicBezierCurve(CommandToken commandToken)
+        private void AddCubicBezierCurve(ref ReadOnlySpan<char> span, bool relative)
         {
-            var point1 = commandToken.IsRelative
-                             ? commandToken.ReadRelativePoint(_currentPoint)
-                             : commandToken.ReadPoint();
+            var point1 = relative
+                    ? ReadRelativePoint(ref span, _currentPoint)
+                    : ReadPoint(ref span);
+
+            span = ReadSeparator(span);
 
-            var point2 = commandToken.IsRelative
-                             ? commandToken.ReadRelativePoint(_currentPoint)
-                             : commandToken.ReadPoint();
+            var point2 = relative
+                    ? ReadRelativePoint(ref span, _currentPoint)
+                    : ReadPoint(ref span);
 
             _previousControlPoint = point2;
 
-            var point3 = commandToken.IsRelative
-                             ? commandToken.ReadRelativePoint(_currentPoint)
-                             : commandToken.ReadPoint();
+            span = ReadSeparator(span);
 
-            if (_isOpen == null)
+            var point3 = relative
+                    ? ReadRelativePoint(ref span, _currentPoint)
+                    : ReadPoint(ref span);
+
+            if (!_isOpen)
             {
                 CreateFigure();
             }
@@ -379,19 +314,21 @@ namespace Avalonia.Media
             _currentPoint = point3;
         }
 
-        private void AddQuadraticBezierCurve(CommandToken commandToken)
+        private void AddQuadraticBezierCurve(ref ReadOnlySpan<char> span, bool relative)
         {
-            var start = commandToken.IsRelative
-                            ? commandToken.ReadRelativePoint(_currentPoint)
-                            : commandToken.ReadPoint();
+            var start = relative
+                    ? ReadRelativePoint(ref span, _currentPoint)
+                    : ReadPoint(ref span);
 
             _previousControlPoint = start;
 
-            var end = commandToken.IsRelative
-                          ? commandToken.ReadRelativePoint(_currentPoint)
-                          : commandToken.ReadPoint();
+            span = ReadSeparator(span);
+
+            var end = relative
+                    ? ReadRelativePoint(ref span, _currentPoint)
+                    : ReadPoint(ref span);
 
-            if (_isOpen == null)
+            if (!_isOpen)
             {
                 CreateFigure();
             }
@@ -401,22 +338,24 @@ namespace Avalonia.Media
             _currentPoint = end;
         }
 
-        private void AddSmoothCubicBezierCurve(CommandToken commandToken)
+        private void AddSmoothCubicBezierCurve(ref ReadOnlySpan<char> span, bool relative)
         {
-            var point2 = commandToken.IsRelative
-                             ? commandToken.ReadRelativePoint(_currentPoint)
-                             : commandToken.ReadPoint();
+            var point2 = relative
+                    ? ReadRelativePoint(ref span, _currentPoint)
+                    : ReadPoint(ref span);
+
+            span = ReadSeparator(span);
 
-            var end = commandToken.IsRelative
-                          ? commandToken.ReadRelativePoint(_currentPoint)
-                          : commandToken.ReadPoint();
+            var end = relative
+                    ? ReadRelativePoint(ref span, _currentPoint)
+                    : ReadPoint(ref span);
 
             if (_previousControlPoint != null)
             {
                 _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint);
             }
 
-            if (_isOpen == null)
+            if (!_isOpen)
             {
                 CreateFigure();
             }
@@ -428,18 +367,18 @@ namespace Avalonia.Media
             _currentPoint = end;
         }
 
-        private void AddSmoothQuadraticBezierCurve(CommandToken commandToken)
+        private void AddSmoothQuadraticBezierCurve(ref ReadOnlySpan<char> span, bool relative)
         {
-            var end = commandToken.IsRelative
-                          ? commandToken.ReadRelativePoint(_currentPoint)
-                          : commandToken.ReadPoint();
+            var end = relative
+                    ? ReadRelativePoint(ref span, _currentPoint)
+                    : ReadPoint(ref span);
 
             if (_previousControlPoint != null)
             {
                 _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint);
             }
 
-            if (_isOpen == null)
+            if (!_isOpen)
             {
                 CreateFigure();
             }
@@ -449,21 +388,27 @@ namespace Avalonia.Media
             _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
-                          ? commandToken.ReadRelativePoint(_currentPoint)
-                          : commandToken.ReadPoint();
+            var sweepDirection = ReadBool(ref span) ? SweepDirection.Clockwise : SweepDirection.CounterClockwise;
+            
+            span = ReadSeparator(span);
 
-            if (_isOpen == null)
+            var end = relative
+                    ? ReadRelativePoint(ref span, _currentPoint)
+                    : ReadPoint(ref span);
+
+            if (!_isOpen)
             {
                 CreateFigure();
             }
@@ -475,210 +420,149 @@ namespace Avalonia.Media
             _previousControlPoint = null;
         }
 
-        private class CommandToken
+        private static bool PeekArgument(ReadOnlySpan<char> span)
         {
-            private const string ArgumentExpression = @"-?[0-9]*\.?\d+";
-
-            private CommandToken(Command command, bool isRelative, IEnumerable<string> arguments)
-            {
-                Command = command;
+            span = SkipWhitespace(span);
 
-                IsRelative = isRelative;
-
-                Arguments = new List<string>(arguments);
-            }
-
-            public Command Command { get; }
-
-            public bool IsRelative { get; }
+            return !span.IsEmpty && (span[0] == ',' || span[0] == '-' || char.IsDigit(span[0]));
+        }
 
-            public bool HasImplicitCommands
+        private static bool ReadArgument(ref ReadOnlySpan<char> remaining, out ReadOnlySpan<char> argument)
+        {
+            remaining = SkipWhitespace(remaining);
+            if (remaining.IsEmpty)
             {
-                get
-                {
-                    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);
-                }
+                argument = ReadOnlySpan<char>.Empty;
+                return false;
             }
 
-            public FillRule ReadFillRule()
+            var valid = false;
+            int i = 0;
+            if (remaining[i] == '-')
             {
-                if (CurrentPosition == Arguments.Count)
-                {
-                    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");
-                }
+                i++;
             }
+            for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true;
 
-            public bool ReadBool()
+            if (i < remaining.Length && remaining[i] == '.')
             {
-                if (CurrentPosition == Arguments.Count)
-                {
-                    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");
-                }
+                valid = false;
+                i++;
             }
+            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");
-                }
-
-                var value = Arguments[CurrentPosition];
-
-                CurrentPosition++;
-
-                return double.Parse(value, CultureInfo.InvariantCulture);
-            }
+                    valid = false;
+                    i++;
+                    if (remaining[i] == '-' || remaining[i] == '+')
+                    {
+                        i++;
+                        for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true;
+                    }                  
+                }               
+            }          
 
-            public Size ReadSize()
+            if (!valid)
             {
-                var width = ReadDouble();
-
-                var height = ReadDouble();
-
-                return new Size(width, height);
+                argument = ReadOnlySpan<char>.Empty;
+                return false;
             }
+            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();
-
-                var y = ReadDouble();
+                span = span.Slice(1);
+            }
+            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);
-
-                var i = reader.Peek();
-
-                if (i == -1)
-                {
+                throw new InvalidDataException("Invalid bool rule.");
+            }
+            
+            switch (boolValue[0])
+            {
+                case '0':
                     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))
-                {
-                    throw new InvalidDataException("Unexpected path command '" + c + "'.");
-                }
+            return double.Parse(doubleValue.ToString(), CultureInfo.InvariantCulture);
+        }
 
-                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;
             }
-
-            private static void ReadWhitespace(TextReader reader)
+            var c = span[0];
+            if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out command))
             {
-                int i;
-
-                while ((i = reader.Peek()) != -1)
-                {
-                    var c = (char)i;
-
-                    if (char.IsWhiteSpace(c))
-                    {
-                        reader.Read();
-                    }
-                    else
-                    {
-                        break;
-                    }
-                }
+                throw new InvalidDataException("Unexpected path command '" + c + "'.");
             }
+            relative = char.IsLower(c);
+            span = span.Slice(1);
+            return true;
         }
     }
 }

+ 42 - 25
src/Avalonia.Visuals/VisualTree/VisualLocator.cs

@@ -1,9 +1,8 @@
 using System;
-using System.Collections.Generic;
 using System.Linq;
 using System.Reactive.Linq;
 using System.Reflection;
-using System.Text;
+using Avalonia.Reactive;
 
 namespace Avalonia.VisualTree
 {
@@ -11,36 +10,54 @@ namespace Avalonia.VisualTree
     {
         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()
-                        .Where(x => ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true)
-                        .ElementAtOrDefault(ancestorLevel);
+                    return _relativeTo.GetVisualAncestors()
+                        .Where(x => _ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true)
+                        .ElementAtOrDefault(_ancestorLevel);
                 }
                 else
                 {
                     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;
+            }
         }
     }
 }

+ 33 - 11
src/Markup/Avalonia.Markup/Data/Binding.cs

@@ -5,11 +5,10 @@ using System;
 using System.Linq;
 using System.Reactive;
 using System.Reactive.Linq;
-using System.Reflection;
-using Avalonia.Controls;
 using Avalonia.Data.Converters;
 using Avalonia.Data.Core;
 using Avalonia.LogicalTree;
+using Avalonia.Reactive;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Data
@@ -190,13 +189,10 @@ namespace Avalonia.Data
 
             if (!targetIsDataContext)
             {
-                var update = target.GetObservable(StyledElement.DataContextProperty)
-                    .Skip(1)
-                    .Select(_ => Unit.Default);
                 var result = new ExpressionObserver(
                     () => target.GetValue(StyledElement.DataContextProperty),
                     path,
-                    update,
+                    new UpdateSignal(target, StyledElement.DataContextProperty),
                     enableDataValidation);
 
                 return result;
@@ -278,14 +274,10 @@ namespace Avalonia.Data
         {
             Contract.Requires<ArgumentNullException>(target != null);
 
-            var update = target.GetObservable(StyledElement.TemplatedParentProperty)
-                .Skip(1)
-                .Select(_ => Unit.Default);
-
             var result = new ExpressionObserver(
                 () => target.GetValue(StyledElement.TemplatedParentProperty),
                 path,
-                update,
+                new UpdateSignal(target, StyledElement.TemplatedParentProperty),
                 enableDataValidation);
 
             return result;
@@ -306,5 +298,35 @@ namespace Avalonia.Data
                            Observable.Return((object)null);
                 }).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);
+                }
+            }
+        }
     }
 }

+ 1 - 1
src/OSX/Avalonia.MonoMac/KeyTransform.cs

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

+ 3 - 0
src/Windows/Avalonia.Win32/ClipboardImpl.cs

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

+ 15 - 0
tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs

@@ -337,6 +337,21 @@ namespace Avalonia.Base.UnitTests.Data.Core
             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 string _stringValue;

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

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

+ 20 - 0
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@@ -315,6 +315,26 @@ namespace Avalonia.Controls.UnitTests
             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]
         public void Empty_Class_Should_Initially_Be_Applied()
         {

+ 18 - 0
tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs

@@ -160,6 +160,24 @@ namespace Avalonia.Controls.UnitTests.Primitives
             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]
         public void Nested_Templated_Control_Should_Not_Have_Template_Applied()
         {

+ 4 - 3
tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs

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

+ 8 - 2
tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs

@@ -17,6 +17,7 @@ namespace Avalonia.Styling.UnitTests
             var source = new TestSubject();
             var target = new ActivatedSubject(activator, source, string.Empty);
 
+            target.Subscribe();
             target.OnNext("bar");
             Assert.Equal(AvaloniaProperty.UnsetValue, source.Value);
             activator.OnNext(true);
@@ -36,6 +37,7 @@ namespace Avalonia.Styling.UnitTests
             var source = new TestSubject();
             var target = new ActivatedSubject(activator, source, string.Empty);
 
+            target.Subscribe();
             activator.OnCompleted();
 
             Assert.True(source.Completed);
@@ -47,10 +49,14 @@ namespace Avalonia.Styling.UnitTests
             var activator = new BehaviorSubject<bool>(false);
             var source = new TestSubject();
             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>

+ 14 - 0
tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs

@@ -40,6 +40,20 @@ namespace Avalonia.Styling.UnitTests
             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]
         public void Should_Unsubscribe_From_Activator_When_All_Subscriptions_Disposed()
         {

+ 24 - 0
tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs

@@ -1,6 +1,7 @@
 // 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.Linq;
 using System.Reactive.Linq;
 using System.Threading.Tasks;
@@ -8,6 +9,7 @@ using Moq;
 using Avalonia.Controls;
 using Avalonia.Styling;
 using Xunit;
+using System.Collections.Generic;
 
 namespace Avalonia.Styling.UnitTests
 {
@@ -117,6 +119,28 @@ namespace Avalonia.Styling.UnitTests
             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
         {
         }

+ 26 - 2
tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs

@@ -7,6 +7,7 @@ using Xunit;
 
 namespace Avalonia.Visuals.UnitTests.Media
 {
+    using System.Globalization;
     using System.IO;
 
     public class PathMarkupParserTests
@@ -69,7 +70,7 @@ namespace Avalonia.Visuals.UnitTests.Media
             using (var context = new PathGeometryContext(pathGeometry))
             using (var parser = new PathMarkupParser(context))
             {
-                parser.Parse("F 1M0,0");             
+                parser.Parse("F 1M0,0");
 
                 Assert.Equal(FillRule.NonZero, pathGeometry.FillRule);
             }
@@ -139,9 +140,32 @@ namespace Avalonia.Visuals.UnitTests.Media
 
                 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]
+        [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("F1 M24,14 A2,2,0,1,1,20,14 A2,2,0,1,1,24,14 z")] // issue #1107
         [InlineData("M0 0L10 10z")]