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
pull/10003/head
Steven Kirk 3 years ago
parent
commit
67c9221d3c
  1. 59
      src/Avalonia.Base/Data/BindingOperations.cs
  2. 43
      src/Avalonia.Base/Data/InstancedBinding.cs
  3. 2
      src/Avalonia.Base/Styling/Setter.cs
  4. 5
      src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs
  5. 4
      src/Markup/Avalonia.Markup/Data/MultiBinding.cs
  6. 6
      tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs
  7. 8
      tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs
  8. 6
      tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs
  9. 4
      tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs

59
src/Avalonia.Base/Data/BindingOperations.cs

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

43
src/Avalonia.Base/Data/InstancedBinding.cs

@ -1,5 +1,6 @@
using System; using System;
using Avalonia.Reactive; using Avalonia.Reactive;
using ObservableEx = Avalonia.Reactive.Observable;
namespace Avalonia.Data namespace Avalonia.Data
{ {
@ -14,11 +15,23 @@ namespace Avalonia.Data
/// </remarks> /// </remarks>
public class InstancedBinding 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; Mode = mode;
Priority = priority; Priority = priority;
Value = value; Source = source ?? throw new ArgumentNullException(nameof(source));
} }
/// <summary> /// <summary>
@ -32,24 +45,12 @@ namespace Avalonia.Data
public BindingPriority Priority { get; } public BindingPriority Priority { get; }
/// <summary> /// <summary>
/// Gets the value or source of the binding. /// Gets the binding source observable.
/// </summary>
public object? Value { get; }
/// <summary>
/// Gets the <see cref="Value"/> as an observable.
/// </summary> /// </summary>
public IObservable<object?>? Observable => Value as IObservable<object?>; public IObservable<object?> Source { get; }
/// <summary> [Obsolete("Use Source property")]
/// Gets the <see cref="Value"/> as an observer. public IObservable<object?> Observable => Source;
/// </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?>;
/// <summary> /// <summary>
/// Creates a new one-time binding with a fixed value. /// Creates a new one-time binding with a fixed value.
@ -61,7 +62,7 @@ namespace Avalonia.Data
object value, object value,
BindingPriority priority = BindingPriority.LocalValue) BindingPriority priority = BindingPriority.LocalValue)
{ {
return new InstancedBinding(value, BindingMode.OneTime, priority); return new InstancedBinding(ObservableEx.SingleValue(value), BindingMode.OneTime, priority);
} }
/// <summary> /// <summary>
@ -106,7 +107,7 @@ namespace Avalonia.Data
{ {
_ = observer ?? throw new ArgumentNullException(nameof(observer)); _ = observer ?? throw new ArgumentNullException(nameof(observer));
return new InstancedBinding(observer, BindingMode.OneWayToSource, priority); return new InstancedBinding((IObservable<object?>)observer, BindingMode.OneWayToSource, priority);
} }
/// <summary> /// <summary>
@ -135,7 +136,7 @@ namespace Avalonia.Data
/// <returns>An <see cref="InstancedBinding"/> instance.</returns> /// <returns>An <see cref="InstancedBinding"/> instance.</returns>
public InstancedBinding WithPriority(BindingPriority priority) public InstancedBinding WithPriority(BindingPriority priority)
{ {
return new InstancedBinding(Value, Mode, priority); return new InstancedBinding(Source, Mode, priority);
} }
} }
} }

2
src/Avalonia.Base/Styling/Setter.cs

@ -109,7 +109,7 @@ namespace Avalonia.Styling
if (mode == BindingMode.OneWay || mode == BindingMode.TwoWay) 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(); throw new NotSupportedException();

5
src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs

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

4
src/Markup/Avalonia.Markup/Data/MultiBinding.cs

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

6
tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs

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

8
tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs

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

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

4
tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs

@ -30,7 +30,7 @@ namespace Avalonia.Markup.UnitTests.Data
}; };
var target = new Control { DataContext = source }; 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); var result = await observable.Take(1);
Assert.Equal("1,2,3", result); Assert.Equal("1,2,3", result);
@ -59,7 +59,7 @@ namespace Avalonia.Markup.UnitTests.Data
}; };
var target = new Control { DataContext = source }; 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); var result = await observable.Take(1);
Assert.Equal("1,2,3", result); Assert.Equal("1,2,3", result);

Loading…
Cancel
Save