Browse Source

Tweaked InstancedBinding API.

- Remove `Value` from the API, will always contain an `IObservable<object?>` from now
- Remove subject from the API, caller can try to cast the observable itself
Steven Kirk 2 years ago
parent
commit
67c9221d3c

+ 23 - 36
src/Avalonia.Base/Data/BindingOperations.cs

@@ -41,54 +41,41 @@ namespace Avalonia.Data
             {
                 case BindingMode.Default:
                 case BindingMode.OneWay:
-                    if (binding.Observable is null)
-                        throw new InvalidOperationException("InstancedBinding does not contain an observable.");
-                    return target.Bind(property, binding.Observable, binding.Priority);
+                    return target.Bind(property, binding.Source, binding.Priority);
                 case BindingMode.TwoWay:
-                    if (binding.Observable is null)
-                        throw new InvalidOperationException("InstancedBinding does not contain an observable.");
-                    if (binding.Subject is null)
+                {
+                    if (binding.Source is not IObserver<object?> observer)
                         throw new InvalidOperationException("InstancedBinding does not contain a subject.");
                     return new TwoWayBindingDisposable(
-                        target.Bind(property, binding.Observable, binding.Priority),
-                        target.GetObservable(property).Subscribe(binding.Subject));
+                        target.Bind(property, binding.Source, binding.Priority),
+                        target.GetObservable(property).Subscribe(observer));
+                }
                 case BindingMode.OneTime:
-                    if (binding.Observable is {} source)
-                    {
-                        // Perf: Avoid allocating closure in the outer scope.
-                        var targetCopy = target;
-                        var propertyCopy = property;
-                        var bindingCopy = binding;
-
-                        return source
-                            .Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue)
-                            .Take(1)
-                            .Subscribe(x => targetCopy.SetValue(
-                                propertyCopy,
-                                BindingNotification.ExtractValue(x),
-                                bindingCopy.Priority));
-                    }
-                    else
-                    {
-                        target.SetValue(property, binding.Value, binding.Priority);
-                        return Disposable.Empty;
-                    }
+                {
+                    // Perf: Avoid allocating closure in the outer scope.
+                    var targetCopy = target;
+                    var propertyCopy = property;
+                    var bindingCopy = binding;
+
+                    return binding.Source
+                        .Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue)
+                        .Take(1)
+                        .Subscribe(x => targetCopy.SetValue(
+                            propertyCopy,
+                            BindingNotification.ExtractValue(x),
+                            bindingCopy.Priority));
+                }
 
                 case BindingMode.OneWayToSource:
                 {
-                    if (binding.Observable is null)
-                        throw new InvalidOperationException("InstancedBinding does not contain an observable.");
-                    if (binding.Subject is null)
+                    if (binding.Source is not IObserver<object?> observer)
                         throw new InvalidOperationException("InstancedBinding does not contain a subject.");
 
-                    // Perf: Avoid allocating closure in the outer scope.
-                    var bindingCopy = binding;
-
                     return Observable.CombineLatest(
-                        binding.Observable,
+                        binding.Source,
                         target.GetObservable(property),
                         (_, v) => v)
-                    .Subscribe(x => bindingCopy.Subject.OnNext(x));
+                    .Subscribe(x => observer.OnNext(x));
                 }
 
                 default:

+ 22 - 21
src/Avalonia.Base/Data/InstancedBinding.cs

@@ -1,5 +1,6 @@
 using System;
 using Avalonia.Reactive;
+using ObservableEx = Avalonia.Reactive.Observable;
 
 namespace Avalonia.Data
 {
@@ -14,11 +15,23 @@ namespace Avalonia.Data
     /// </remarks>
     public class InstancedBinding
     {
-        internal InstancedBinding(object? value, BindingMode mode, BindingPriority priority)
+        /// <summary>
+        /// Initializes a new instance of the <see cref="InstancedBinding"/> class.
+        /// </summary>
+        /// <param name="source">The binding source.</param>
+        /// <param name="mode">The binding mode.</param>
+        /// <param name="priority">The priority of the binding.</param>
+        /// <remarks>
+        /// This constructor can be used to create any type of binding and as such requires an
+        /// <see cref="ISubject{Object}"/> as the binding source because this is the only binding
+        /// source which can be used for all binding modes. If you wish to create an instance with
+        /// something other than a subject, use one of the static creation methods on this class.
+        /// </remarks>
+        internal InstancedBinding(IObservable<object?> source, BindingMode mode, BindingPriority priority)
         {
             Mode = mode;
             Priority = priority;
-            Value = value;
+            Source = source ?? throw new ArgumentNullException(nameof(source));
         }
 
         /// <summary>
@@ -32,24 +45,12 @@ namespace Avalonia.Data
         public BindingPriority Priority { get; }
 
         /// <summary>
-        /// Gets the value or source of the binding.
-        /// </summary>
-        public object? Value { get; }
-
-        /// <summary>
-        /// Gets the <see cref="Value"/> as an observable.
+        /// Gets the binding source observable.
         /// </summary>
-        public IObservable<object?>? Observable => Value as IObservable<object?>;
+        public IObservable<object?> Source { get; }
 
-        /// <summary>
-        /// Gets the <see cref="Value"/> as an observer.
-        /// </summary>
-        public IObserver<object?>? Observer => Value as IObserver<object?>;
-
-        /// <summary>
-        /// Gets the <see cref="Subject"/> as an subject.
-        /// </summary>
-        internal IAvaloniaSubject<object?>? Subject => Value as IAvaloniaSubject<object?>;
+        [Obsolete("Use Source property")]
+        public IObservable<object?> Observable => Source;
 
         /// <summary>
         /// Creates a new one-time binding with a fixed value.
@@ -61,7 +62,7 @@ namespace Avalonia.Data
             object value,
             BindingPriority priority = BindingPriority.LocalValue)
         {
-            return new InstancedBinding(value, BindingMode.OneTime, priority);
+            return new InstancedBinding(ObservableEx.SingleValue(value), BindingMode.OneTime, priority);
         }
 
         /// <summary>
@@ -106,7 +107,7 @@ namespace Avalonia.Data
         {
             _ = observer ?? throw new ArgumentNullException(nameof(observer));
 
-            return new InstancedBinding(observer, BindingMode.OneWayToSource, priority);
+            return new InstancedBinding((IObservable<object?>)observer, BindingMode.OneWayToSource, priority);
         }
 
         /// <summary>
@@ -135,7 +136,7 @@ namespace Avalonia.Data
         /// <returns>An <see cref="InstancedBinding"/> instance.</returns>
         public InstancedBinding WithPriority(BindingPriority priority)
         {
-            return new InstancedBinding(Value, Mode, priority);
+            return new InstancedBinding(Source, Mode, priority);
         }
     }
 }

+ 1 - 1
src/Avalonia.Base/Styling/Setter.cs

@@ -109,7 +109,7 @@ namespace Avalonia.Styling
 
                 if (mode == BindingMode.OneWay || mode == BindingMode.TwoWay)
                 {
-                    return new PropertySetterBindingInstance(target, instance, Property, mode, i.Observable!);
+                    return new PropertySetterBindingInstance(target, instance, Property, mode, i.Source);
                 }
 
                 throw new NotSupportedException();

+ 3 - 2
src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs

@@ -7,6 +7,7 @@ using Avalonia.Data;
 using System;
 using Avalonia.Controls.Utils;
 using Avalonia.Markup.Xaml.MarkupExtensions;
+using Avalonia.Reactive;
 
 namespace Avalonia.Controls
 {
@@ -111,9 +112,9 @@ namespace Avalonia.Controls
 
             if (result != null)
             {
-                if(result.Subject != null)
+                if(result.Source is IAvaloniaSubject<object> subject)
                 {
-                    var bindingHelper = new CellEditBinding(result.Subject);
+                    var bindingHelper = new CellEditBinding(subject);
                     var instanceBinding = new InstancedBinding(bindingHelper.InternalSubject, result.Mode, result.Priority); 
 
                     BindingOperations.Apply(target, property, instanceBinding, null);

+ 2 - 2
src/Markup/Avalonia.Markup/Data/MultiBinding.cs

@@ -85,8 +85,8 @@ namespace Avalonia.Data
 
             var children = Bindings.Select(x => x.Initiate(target, null));
 
-            var input = children.Select(x => x?.Observable!)
-                                .Where(x => x is not null)
+            var input = children.Select(x => x?.Source)
+                                .Where(x => x is not null)!
                                 .CombineLatest()
                                 .Select(x => ConvertValue(x, targetType, converter))
                                 .Where(x => x != BindingOperations.DoNothing);

+ 3 - 3
tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs

@@ -334,7 +334,7 @@ namespace Avalonia.Markup.UnitTests.Data
                 Path = "Foo",
             };
 
-            var result = binding.Initiate(target, TextBox.TextProperty).Value;
+            var result = binding.Initiate(target, TextBox.TextProperty).Source;
 
             Assert.IsType<DefaultValueConverter>(((BindingExpression)result).Converter);
         }
@@ -350,7 +350,7 @@ namespace Avalonia.Markup.UnitTests.Data
                 Path = "Foo",
             };
 
-            var result = binding.Initiate(target, TextBox.TextProperty).Value;
+            var result = binding.Initiate(target, TextBox.TextProperty).Source;
 
             Assert.Same(converter.Object, ((BindingExpression)result).Converter);
         }
@@ -367,7 +367,7 @@ namespace Avalonia.Markup.UnitTests.Data
                 Path = "Bar",
             };
 
-            var result = binding.Initiate(target, TextBox.TextProperty).Value;
+            var result = binding.Initiate(target, TextBox.TextProperty).Source;
 
             Assert.Same("foo", ((BindingExpression)result).ConverterParameter);
         }

+ 4 - 4
tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs

@@ -24,7 +24,7 @@ namespace Avalonia.Markup.UnitTests.Data
 
             var expressionObserver = (BindingExpression)target.Initiate(
                 textBlock, 
-                TextBlock.TextProperty).Observable;
+                TextBlock.TextProperty).Source;
 
             Assert.Same(StringConverters.IsNullOrEmpty, expressionObserver.Converter);
         }
@@ -46,7 +46,7 @@ namespace Avalonia.Markup.UnitTests.Data
 
                 var expressionObserver = (BindingExpression)target.Initiate(
                     textBlock,
-                    TextBlock.TextProperty).Observable;
+                    TextBlock.TextProperty).Source;
 
                 Assert.IsType<StringFormatValueConverter>(expressionObserver.Converter);
             }
@@ -69,7 +69,7 @@ namespace Avalonia.Markup.UnitTests.Data
 
                 var expressionObserver = (BindingExpression)target.Initiate(
                     textBlock,
-                    TextBlock.TagProperty).Observable;
+                    TextBlock.TagProperty).Source;
 
                 Assert.IsType<StringFormatValueConverter>(expressionObserver.Converter);
             }
@@ -92,7 +92,7 @@ namespace Avalonia.Markup.UnitTests.Data
 
                 var expressionObserver = (BindingExpression)target.Initiate(
                     textBlock,
-                    TextBlock.MarginProperty).Observable;
+                    TextBlock.MarginProperty).Source;
 
                 Assert.Same(DefaultValueConverter.Instance, expressionObserver.Converter);
             }

+ 3 - 3
tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs

@@ -20,7 +20,7 @@ namespace Avalonia.Markup.UnitTests.Data
 
             var target = new Binding(nameof(Class1.Foo));
             var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: false);
-            var subject = (BindingExpression)instanced.Value;
+            var subject = (BindingExpression)instanced.Source;
             object result = null;
 
             subject.Subscribe(x => result = x);
@@ -38,7 +38,7 @@ namespace Avalonia.Markup.UnitTests.Data
 
             var target = new Binding(nameof(Class1.Foo));
             var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true);
-            var subject = (BindingExpression)instanced.Value;
+            var subject = (BindingExpression)instanced.Source;
             object result = null;
 
             subject.Subscribe(x => result = x);
@@ -56,7 +56,7 @@ namespace Avalonia.Markup.UnitTests.Data
 
             var target = new Binding(nameof(Class1.Foo)) { Priority = BindingPriority.Template };
             var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true);
-            var subject = (BindingExpression)instanced.Value;
+            var subject = (BindingExpression)instanced.Source;
             object result = null;
 
             subject.Subscribe(x => result = x);

+ 2 - 2
tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs

@@ -30,7 +30,7 @@ namespace Avalonia.Markup.UnitTests.Data
             };
 
             var target = new Control { DataContext = source };
-            var observable = binding.Initiate(target, null).Observable;
+            var observable = binding.Initiate(target, null).Source;
             var result = await observable.Take(1);
 
             Assert.Equal("1,2,3", result);
@@ -59,7 +59,7 @@ namespace Avalonia.Markup.UnitTests.Data
             };
 
             var target = new Control { DataContext = source };
-            var observable = binding.Initiate(target, null).Observable;
+            var observable = binding.Initiate(target, null).Source;
             var result = await observable.Take(1);
 
             Assert.Equal("1,2,3", result);