Sfoglia il codice sorgente

Refactored InstancedBinding creation.

Previously it wasn't clear which constructor on `InstancedBinding` must be called for a particular binding mode. Refactored the constructors:

- We now have a single public ctor which takes an `ISubject`, as a subject can be used for all binding modes.
- Instanced bindings with other sources such as single values and `IObservables` are now constructed via static methods which only allow the correct sources for that binding mode
Steven Kirk 8 anni fa
parent
commit
d6b6a0fa96

+ 1 - 1
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@@ -326,7 +326,7 @@ namespace Avalonia
                 object anchor = null,
                 bool enableDataValidation = false)
             {
-                return new InstancedBinding(_source);
+                return InstancedBinding.OneWay(_source);
             }
         }
     }

+ 11 - 6
src/Avalonia.Base/Data/IndexerBinding.cs

@@ -31,13 +31,18 @@ namespace Avalonia.Data
                 targetProperty.GetMetadata(target.GetType()).DefaultBindingMode :
                 Mode;
 
-            if (mode == BindingMode.TwoWay)
+            switch (mode)
             {
-                return new InstancedBinding(Source.GetSubject(Property), mode);
-            }
-            else
-            {
-                return new InstancedBinding(Source.GetObservable(Property), mode);
+                case BindingMode.OneTime:
+                    return InstancedBinding.OneTime(Source.GetObservable(Property));
+                case BindingMode.OneWay:
+                    return InstancedBinding.OneWay(Source.GetObservable(Property));
+                case BindingMode.OneWayToSource:
+                    return InstancedBinding.OneWayToSource(Source.GetSubject(Property));
+                case BindingMode.TwoWay:
+                    return InstancedBinding.TwoWay(Source.GetSubject(Property));
+                default:
+                    throw new NotSupportedException("Unsupported BindingMode.");
             }
         }
     }

+ 101 - 54
src/Avalonia.Base/Data/InstancedBinding.cs

@@ -14,71 +14,35 @@ namespace Avalonia.Data
     /// property on a control's DataContext"; this class represents a binding that has been 
     /// *instanced* by calling <see cref="IBinding.Initiate(IAvaloniaObject, AvaloniaProperty, object, bool)"/>
     /// on a target object.
-    /// 
-    /// When a binding is initiated, it can return one of 3 possible sources for the binding:
-    /// - An <see cref="ISubject{Object}"/> which can be used for any type of binding.
-    /// - An <see cref="IObservable{Object}"/> which can be used for all types of bindings except
-    ///  <see cref="BindingMode.OneWayToSource"/> and <see cref="BindingMode.TwoWay"/>.
-    /// - A plain object, which can only represent a <see cref="BindingMode.OneTime"/> binding.
     /// </remarks>
     public class InstancedBinding
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="InstancedBinding"/> class.
         /// </summary>
-        /// <param name="value">
-        /// The value used for the <see cref="BindingMode.OneTime"/> binding.
-        /// </param>
-        /// <param name="priority">The binding priority.</param>
-        public InstancedBinding(object value,
-            BindingPriority priority = BindingPriority.LocalValue)
-        {
-            Mode = BindingMode.OneTime;
-            Priority = priority;
-            Value = value;
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="InstancedBinding"/> class.
-        /// </summary>
-        /// <param name="observable">The observable for a one-way binding.</param>
+        /// <param name="subject">The binding source.</param>
         /// <param name="mode">The binding mode.</param>
-        /// <param name="priority">The binding priority.</param>
-        public InstancedBinding(
-            IObservable<object> observable, 
-            BindingMode mode = BindingMode.OneWay,
-            BindingPriority priority = BindingPriority.LocalValue)
+        /// <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>
+        public InstancedBinding(ISubject<object> subject, BindingMode mode, BindingPriority priority)
         {
-            Contract.Requires<ArgumentNullException>(observable != null);
-
-            if (mode == BindingMode.OneWayToSource || mode == BindingMode.TwoWay)
-            {
-                throw new ArgumentException(
-                    "Invalid BindingResult mode: OneWayToSource and TwoWay bindings" +
-                    "require a Subject.");
-            }
+            Contract.Requires<ArgumentNullException>(subject != null);
 
             Mode = mode;
             Priority = priority;
-            Observable = observable;
+            Value = subject;
         }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="InstancedBinding"/> class.
-        /// </summary>
-        /// <param name="subject">The subject for a two-way binding.</param>
-        /// <param name="mode">The binding mode.</param>
-        /// <param name="priority">The binding priority.</param>
-        public InstancedBinding(
-            ISubject<object> subject,
-            BindingMode mode = BindingMode.OneWay,
-            BindingPriority priority = BindingPriority.LocalValue)
+        private InstancedBinding(object value, BindingMode mode, BindingPriority priority)
         {
-            Contract.Requires<ArgumentNullException>(subject != null);
-
             Mode = mode;
             Priority = priority;
-            Subject = subject;
+            Value = value;
         }
 
         /// <summary>
@@ -92,18 +56,101 @@ namespace Avalonia.Data
         public BindingPriority Priority { get; }
 
         /// <summary>
-        /// Gets the value used for a <see cref="BindingMode.OneTime"/> binding.
+        /// Gets the value or source of the binding.
         /// </summary>
         public object Value { get; }
 
         /// <summary>
-        /// Gets the observable for a one-way binding.
+        /// Gets the <see cref="Value"/> as an observable.
         /// </summary>
-        public IObservable<object> Observable { get; }
+        public IObservable<object> Observable => Value as IObservable<object>;
 
         /// <summary>
-        /// Gets the subject for a two-way binding.
+        /// Gets the <see cref="Value"/> as a subject.
         /// </summary>
-        public ISubject<object> Subject { get; }
+        public ISubject<object> Subject => Value as ISubject<object>;
+
+        /// <summary>
+        /// Creates a new one-time binding with a fixed value.
+        /// </summary>
+        /// <param name="value">The value.</param>
+        /// <param name="priority">The priority of the binding.</param>
+        /// <returns>An <see cref="InstancedBinding"/> instance.</returns>
+        public static InstancedBinding OneTime(
+            object value,
+            BindingPriority priority = BindingPriority.LocalValue)
+        {
+            return new InstancedBinding(value, BindingMode.OneTime, priority);
+        }
+
+        /// <summary>
+        /// Creates a new one-time binding.
+        /// </summary>
+        /// <param name="observable">The source observable.</param>
+        /// <param name="priority">The priority of the binding.</param>
+        /// <returns>An <see cref="InstancedBinding"/> instance.</returns>
+        public static InstancedBinding OneTime(
+            IObservable<object> observable,
+            BindingPriority priority = BindingPriority.LocalValue)
+        {
+            Contract.Requires<ArgumentNullException>(observable != null);
+
+            return new InstancedBinding(observable, BindingMode.OneTime, priority);
+        }
+
+        /// <summary>
+        /// Creates a new one-way binding.
+        /// </summary>
+        /// <param name="observable">The source observable.</param>
+        /// <param name="priority">The priority of the binding.</param>
+        /// <returns>An <see cref="InstancedBinding"/> instance.</returns>
+        public static InstancedBinding OneWay(
+            IObservable<object> observable,
+            BindingPriority priority = BindingPriority.LocalValue)
+        {
+            Contract.Requires<ArgumentNullException>(observable != null);
+
+            return new InstancedBinding(observable, BindingMode.OneWay, priority);
+        }
+
+        /// <summary>
+        /// Creates a new one-way to source binding.
+        /// </summary>
+        /// <param name="subject">The binding source.</param>
+        /// <param name="priority">The priority of the binding.</param>
+        /// <returns>An <see cref="InstancedBinding"/> instance.</returns>
+        public static InstancedBinding OneWayToSource(
+            ISubject<object> subject,
+            BindingPriority priority = BindingPriority.LocalValue)
+        {
+            Contract.Requires<ArgumentNullException>(subject != null);
+
+            return new InstancedBinding(subject, BindingMode.OneWayToSource, priority);
+        }
+
+        /// <summary>
+        /// Creates a new two-way binding.
+        /// </summary>
+        /// <param name="subject">The binding source.</param>
+        /// <param name="priority">The priority of the binding.</param>
+        /// <returns>An <see cref="InstancedBinding"/> instance.</returns>
+        public static InstancedBinding TwoWay(
+            ISubject<object> subject,
+            BindingPriority priority = BindingPriority.LocalValue)
+        {
+            Contract.Requires<ArgumentNullException>(subject != null);
+
+            return new InstancedBinding(subject, BindingMode.TwoWay, priority);
+        }
+
+        /// <summary>
+        /// Creates a copy of the <see cref="InstancedBinding"/> with a different priority.
+        /// </summary>
+        /// <param name="priority">The priority of the binding.</param>
+        /// <returns>An <see cref="InstancedBinding"/> instance.</returns>
+        public InstancedBinding WithPriority(BindingPriority priority)
+        {
+            return new InstancedBinding(Value, Mode, priority);
+        }
     }
 }

+ 1 - 1
src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs

@@ -62,7 +62,7 @@ namespace Avalonia.Controls.Templates
         /// <returns>The child items, or null if no child items.</returns>
         public InstancedBinding ItemsSelector(object item)
         {
-            return new InstancedBinding(this?._itemsSelector(item));
+            return InstancedBinding.OneTime(this?._itemsSelector(item));
         }
 
         /// <summary>

+ 31 - 29
src/Avalonia.Styling/Styling/Setter.cs

@@ -135,45 +135,47 @@ namespace Avalonia.Styling
 
         private InstancedBinding Clone(InstancedBinding sourceInstance, IStyle style, IObservable<bool> activator)
         {
-            InstancedBinding cloned;
-
             if (activator != null)
             {
                 var description = style?.ToString();
 
-                if (sourceInstance.Mode == BindingMode.TwoWay || sourceInstance.Mode == BindingMode.OneWayToSource)
-                {
-                    var activated = new ActivatedSubject(activator, sourceInstance.Subject, description);
-                    cloned = new InstancedBinding(activated, sourceInstance.Mode, BindingPriority.StyleTrigger);
-                }
-                else if (sourceInstance.Mode == BindingMode.OneTime)
-                {
-                    var activated = new ActivatedValue(activator, sourceInstance.Value, description);
-                    cloned = new InstancedBinding(activated, BindingMode.OneWay, BindingPriority.StyleTrigger);
-                }
-                else
+                switch (sourceInstance.Mode)
                 {
-                    var activated = new ActivatedObservable(activator, sourceInstance.Observable ?? sourceInstance.Subject, description);
-                    cloned = new InstancedBinding(activated, sourceInstance.Mode, BindingPriority.StyleTrigger);
+                    case BindingMode.OneTime:
+                        if (sourceInstance.Observable != null)
+                        {
+                            var activated = new ActivatedObservable(activator, sourceInstance.Observable, description);
+                            return InstancedBinding.OneTime(activated, BindingPriority.StyleTrigger);
+                        }
+                        else
+                        {
+                            var activated = new ActivatedValue(activator, sourceInstance.Value, description);
+                            return InstancedBinding.OneTime(activated, BindingPriority.StyleTrigger);
+                        }
+                    case BindingMode.OneWay:
+                        {
+                            var activated = new ActivatedObservable(activator, sourceInstance.Observable, description);
+                            return InstancedBinding.OneWay(activated, BindingPriority.StyleTrigger);
+                        }
+                    case BindingMode.OneWayToSource:
+                        {
+                            var activated = new ActivatedSubject(activator, sourceInstance.Subject, description);
+                            return InstancedBinding.OneWayToSource(activated, BindingPriority.StyleTrigger);
+                        }
+                    case BindingMode.TwoWay:
+                        {
+                            var activated = new ActivatedSubject(activator, sourceInstance.Subject, description);
+                            return InstancedBinding.TwoWay(activated, BindingPriority.StyleTrigger);
+                        }
+                    default:
+                        throw new NotSupportedException("Unsupported BindingMode.");
                 }
+
             }
             else
             {
-                if (sourceInstance.Subject != null)
-                {
-                    cloned = new InstancedBinding(sourceInstance.Subject, sourceInstance.Mode, BindingPriority.Style);
-                }
-                else if (sourceInstance.Observable != null)
-                {
-                    cloned = new InstancedBinding(sourceInstance.Observable, sourceInstance.Mode, BindingPriority.Style);
-                }
-                else
-                {
-                    cloned = new InstancedBinding(sourceInstance.Value, BindingPriority.Style);
-                }
+                return sourceInstance.WithPriority(BindingPriority.StyleTrigger);
             }
-
-            return cloned;
         }
     }
 }

+ 7 - 28
src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs

@@ -62,41 +62,20 @@ namespace Avalonia.Markup.Xaml.Data
             }
 
             var targetType = targetProperty?.PropertyType ?? typeof(object);
-            var result = new BehaviorSubject<object>(AvaloniaProperty.UnsetValue);
             var children = Bindings.Select(x => x.Initiate(target, null));
             var input = children.Select(x => x.Subject).CombineLatest().Select(x => ConvertValue(x, targetType));
-            input.Subscribe(result);
-            return new InstancedBinding(result, Mode, Priority);
-        }
-
-        /// <summary>
-        /// Applies a binding subject to a property on an instance.
-        /// </summary>
-        /// <param name="target">The target instance.</param>
-        /// <param name="property">The target property.</param>
-        /// <param name="subject">The binding subject.</param>
-        internal void Bind(IAvaloniaObject target, AvaloniaProperty property, ISubject<object> subject)
-        {
             var mode = Mode == BindingMode.Default ?
-                property.GetMetadata(target.GetType()).DefaultBindingMode : Mode;
+                targetProperty.GetMetadata(target.GetType()).DefaultBindingMode : Mode;
 
             switch (mode)
             {
-                case BindingMode.Default:
-                case BindingMode.OneWay:
-                    target.Bind(property, subject, Priority);
-                    break;
-                case BindingMode.TwoWay:
-                    throw new NotSupportedException("TwoWay MultiBinding not currently supported.");
                 case BindingMode.OneTime:
-                    target.GetObservable(Control.DataContextProperty).Subscribe(dataContext =>
-                    {
-                        subject.Take(1).Subscribe(x => target.SetValue(property, x, Priority));
-                    });                    
-                    break;
-                case BindingMode.OneWayToSource:
-                    target.GetObservable(property).Subscribe(subject);
-                    break;
+                    return InstancedBinding.OneTime(input, Priority);
+                case BindingMode.OneWay:
+                    return InstancedBinding.OneWay(input, Priority);
+                default:
+                    throw new NotSupportedException(
+                        "MultiBinding currently only supports OneTime and OneWay BindingMode.");
             }
         }
 

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs

@@ -58,7 +58,7 @@ namespace Avalonia.Markup.Xaml.Data
 
             if (resource != AvaloniaProperty.UnsetValue)
             {
-                return new InstancedBinding(resource, Priority);
+                return InstancedBinding.OneTime(resource, Priority);
             }
             else
             {

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs

@@ -51,7 +51,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
 
             if (control != null)
             {
-                return new InstancedBinding(control.GetResourceObservable(ResourceKey));
+                return InstancedBinding.OneWay(control.GetResourceObservable(ResourceKey));
             }
 
             return null;

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs

@@ -42,7 +42,7 @@ namespace Avalonia.Markup.Xaml.Templates
             if (ItemsSource != null)
             {
                 var obs = new ExpressionObserver(item, ItemsSource.Path);
-                return new InstancedBinding(obs, BindingMode.OneWay, BindingPriority.Style);
+                return InstancedBinding.OneWay(obs, BindingPriority.Style);
             }
 
             return null;

+ 1 - 1
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

@@ -428,7 +428,7 @@ namespace Avalonia.Base.UnitTests
                 object anchor = null,
                 bool enableDataValidation = false)
             {
-                return new InstancedBinding(_source, BindingMode.OneTime);
+                return InstancedBinding.OneTime(_source);
             }
         }
     }

+ 1 - 1
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@@ -515,7 +515,7 @@ namespace Avalonia.Controls.UnitTests
             public InstancedBinding ItemsSelector(object item)
             {
                 var obs = new ExpressionObserver(item, nameof(Node.Children));
-                return new InstancedBinding(obs);
+                return InstancedBinding.OneWay(obs);
             }
 
             public bool Match(object data)

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

@@ -34,8 +34,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
             var target = new Mock<IAvaloniaObject>().As<IControl>();
             target.Setup(x => x.GetValue(Control.DataContextProperty)).Returns(source);
 
-            var subject = binding.Initiate(target.Object, null).Subject;
-            var result = await subject.Take(1);
+            var observable = binding.Initiate(target.Object, null).Observable;
+            var result = await observable.Take(1);
 
             Assert.Equal("1,2,3", result);
         }

+ 1 - 1
tests/Avalonia.Styling.UnitTests/SetterTests.cs

@@ -29,7 +29,7 @@ namespace Avalonia.Styling.UnitTests
         {
             var control = new TextBlock();
             var subject = new BehaviorSubject<object>("foo");
-            var descriptor = new InstancedBinding(subject);
+            var descriptor = InstancedBinding.OneWay(subject);
             var binding = Mock.Of<IBinding>(x => x.Initiate(control, TextBlock.TextProperty, null, false) == descriptor);
             var style = Mock.Of<IStyle>();
             var setter = new Setter(TextBlock.TextProperty, binding);