diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index e54dc13a83..05753738cb 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -7,6 +7,7 @@ using Avalonia.Data.Core; using Avalonia.Diagnostics; using Avalonia.Logging; using Avalonia.PropertyStore; +using Avalonia.Reactive; using Avalonia.Threading; namespace Avalonia @@ -407,6 +408,24 @@ namespace Avalonia } } + /// + /// Binds a to an . + /// + /// The property. + /// The binding. + /// + /// A disposable which can be used to terminate the binding. + /// + public IBindingExpression Bind(AvaloniaProperty property, IBinding binding) + { + if (binding is not IBinding2 b) + throw new NotSupportedException($"Unsupported IBinding implementation '{binding}'."); + if (b.Instance(this, property) is not UntypedBindingExpressionBase expression) + throw new NotSupportedException("Binding returned unsupported IBindingExpression."); + + return property.RouteBind(this, expression); + } + /// /// Binds a to an observable. /// @@ -611,10 +630,9 @@ namespace Avalonia internal IDisposable Bind( AvaloniaProperty property, - UntypedBindingExpressionBase expression, - BindingPriority priority) + UntypedBindingExpressionBase expression) { - return property.RouteBind(this, expression, priority); + return property.RouteBind(this, expression); } /// diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 7becd1c990..0e7ccbf4f3 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Data; +using Avalonia.Data.Core; using Avalonia.Reactive; namespace Avalonia @@ -233,9 +234,10 @@ namespace Avalonia /// An optional anchor from which to locate required context. When binding to objects that /// are not in the logical tree, certain types of binding need an anchor into the tree in /// order to locate named controls or resources. The parameter - /// can be used to provice this context. + /// can be used to provide this context. /// /// An which can be used to cancel the binding. + [Obsolete] public static IDisposable Bind( this AvaloniaObject target, AvaloniaProperty property, @@ -367,7 +369,7 @@ namespace Avalonia return observable.Subscribe(new ClassHandlerObserver(action)); } - private class BindingAdaptor : IBinding + private class BindingAdaptor : IBinding2 { private readonly IObservable _source; @@ -382,7 +384,13 @@ namespace Avalonia object? anchor = null, bool enableDataValidation = false) { - return InstancedBinding.OneWay(_source); + var expression = new UntypedObservableBindingExpression(_source); + return new InstancedBinding(expression, BindingMode.OneWay, BindingPriority.LocalValue); + } + + IBindingExpression IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property) + { + return new UntypedObservableBindingExpression(_source); } } diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 934bb29cde..1b32e4699e 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -566,11 +566,9 @@ namespace Avalonia /// /// The object instance. /// The binding source. - /// The priority. - internal abstract IDisposable RouteBind( + internal abstract IBindingExpression RouteBind( AvaloniaObject o, - UntypedBindingExpressionBase source, - BindingPriority priority); + UntypedBindingExpressionBase source); /// /// Overrides the metadata for the property on the specified type. diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index c3463e4c51..a2194fae1d 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -1,5 +1,5 @@ using System; -using Avalonia.Data.Core; +using Avalonia.Diagnostics; using Avalonia.Reactive; namespace Avalonia.Data @@ -15,6 +15,7 @@ namespace Avalonia.Data /// The property to bind. /// The instanced binding. /// An which can be used to cancel the binding. + [Obsolete(ObsoletionMessages.MayBeRemovedInAvalonia12)] public static IDisposable Apply( AvaloniaObject target, AvaloniaProperty property, @@ -26,7 +27,7 @@ namespace Avalonia.Data if (binding.Expression is { } expression) { - return target.Bind(property, expression, binding.Priority); + return target.Bind(property, expression); } var mode = binding.Mode; diff --git a/src/Avalonia.Base/Data/Core/IBinding2.cs b/src/Avalonia.Base/Data/Core/IBinding2.cs new file mode 100644 index 0000000000..23ff3808ee --- /dev/null +++ b/src/Avalonia.Base/Data/Core/IBinding2.cs @@ -0,0 +1,6 @@ +namespace Avalonia.Data.Core; + +internal interface IBinding2 : IBinding +{ + IBindingExpression Instance(AvaloniaObject target, AvaloniaProperty targetProperty); +} diff --git a/src/Avalonia.Base/Data/Core/IndexerBindingExpression.cs b/src/Avalonia.Base/Data/Core/IndexerBindingExpression.cs new file mode 100644 index 0000000000..b126c91027 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/IndexerBindingExpression.cs @@ -0,0 +1,67 @@ +using System; + +namespace Avalonia.Data.Core; + +internal class IndexerBindingExpression : UntypedBindingExpressionBase +{ + private readonly AvaloniaObject _source; + private readonly AvaloniaProperty _sourceProperty; + private readonly AvaloniaObject _target; + private readonly AvaloniaProperty? _targetProperty; + private readonly BindingMode _mode; + + public IndexerBindingExpression( + AvaloniaObject source, + AvaloniaProperty sourceProperty, + AvaloniaObject target, + AvaloniaProperty? targetProperty, + BindingMode mode) + { + _source = source; + _sourceProperty = sourceProperty; + _target = target; + _targetProperty = targetProperty; + _mode = mode; + } + + public override string Description => $"IndexerBinding {_sourceProperty})"; + + internal override bool WriteValueToSource(object? value) + { + _source.SetValue(_sourceProperty, value); + return true; + } + + protected override void StartCore() + { + if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource && _targetProperty is not null) + _target.PropertyChanged += OnTargetPropertyChanged; + + if (_mode is not BindingMode.OneWayToSource) + { + _source.PropertyChanged += OnSourcePropertyChanged; + PublishValue(_source.GetValue(_sourceProperty)); + } + + if (_mode is BindingMode.OneTime) + Stop(); + } + + protected override void StopCore() + { + _source.PropertyChanged -= OnSourcePropertyChanged; + _target.PropertyChanged -= OnTargetPropertyChanged; + } + + private void OnSourcePropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _sourceProperty) + PublishValue(_source.GetValue(_sourceProperty)); + } + + private void OnTargetPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _targetProperty) + WriteValueToSource(e.NewValue); + } +} diff --git a/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs b/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs index 07ccd32c06..a0ea7b471c 100644 --- a/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs +++ b/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs @@ -15,7 +15,8 @@ namespace Avalonia.Data.Core; /// Base class for binding expressions which produce untyped values. /// [PrivateApi] -public abstract class UntypedBindingExpressionBase : BindingExpressionBase, +public abstract class UntypedBindingExpressionBase : BindingExpressionBase, + IBindingExpression, IDisposable, IDescription, IValueEntry diff --git a/src/Avalonia.Base/Data/Core/UntypedObservableBindingExpression.cs b/src/Avalonia.Base/Data/Core/UntypedObservableBindingExpression.cs new file mode 100644 index 0000000000..5cfac3d443 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/UntypedObservableBindingExpression.cs @@ -0,0 +1,32 @@ +using System; + +namespace Avalonia.Data.Core; + +internal class UntypedObservableBindingExpression : UntypedBindingExpressionBase, IObserver +{ + private readonly IObservable _observable; + private IDisposable? _subscription; + + public UntypedObservableBindingExpression( + IObservable observable) + { + _observable = observable; + } + + public override string Description => "Observable"; + + protected override void StartCore() + { + _subscription = _observable.Subscribe(this); + } + + protected override void StopCore() + { + _subscription?.Dispose(); + _subscription = null; + } + + void IObserver.OnCompleted() { } + void IObserver.OnError(Exception error) { } + void IObserver.OnNext(object? value) => PublishValue(value); +} diff --git a/src/Avalonia.Base/Data/IBinding.cs b/src/Avalonia.Base/Data/IBinding.cs index 01204c39c1..e000da18ff 100644 --- a/src/Avalonia.Base/Data/IBinding.cs +++ b/src/Avalonia.Base/Data/IBinding.cs @@ -1,3 +1,5 @@ +using System; +using Avalonia.Diagnostics; using Avalonia.Metadata; namespace Avalonia.Data @@ -23,6 +25,7 @@ namespace Avalonia.Data /// /// A or null if the binding could not be resolved. /// + [Obsolete(ObsoletionMessages.MayBeRemovedInAvalonia12)] InstancedBinding? Initiate( AvaloniaObject target, AvaloniaProperty? targetProperty, diff --git a/src/Avalonia.Base/Data/IBindingExpression.cs b/src/Avalonia.Base/Data/IBindingExpression.cs new file mode 100644 index 0000000000..8ab35c61f8 --- /dev/null +++ b/src/Avalonia.Base/Data/IBindingExpression.cs @@ -0,0 +1,7 @@ +using System; + +namespace Avalonia.Data; + +public interface IBindingExpression : IDisposable +{ +} diff --git a/src/Avalonia.Base/Data/IndexerBinding.cs b/src/Avalonia.Base/Data/IndexerBinding.cs index a1b9e8b151..8e8bda045d 100644 --- a/src/Avalonia.Base/Data/IndexerBinding.cs +++ b/src/Avalonia.Base/Data/IndexerBinding.cs @@ -1,8 +1,10 @@ -using Avalonia.Reactive; +using System; +using Avalonia.Data.Core; +using Avalonia.Diagnostics; namespace Avalonia.Data { - internal class IndexerBinding : IBinding + internal class IndexerBinding : IBinding2 { public IndexerBinding( AvaloniaObject source, @@ -18,16 +20,20 @@ namespace Avalonia.Data public AvaloniaProperty Property { get; } private BindingMode Mode { get; } + [Obsolete(ObsoletionMessages.MayBeRemovedInAvalonia12)] public InstancedBinding? Initiate( AvaloniaObject target, AvaloniaProperty? targetProperty, object? anchor = null, bool enableDataValidation = false) { - var subject = new CombinedSubject( - new AnonymousObserver(x => Source.SetValue(Property, x, BindingPriority.LocalValue)), - Source.GetObservable(Property)); - return new InstancedBinding(subject, Mode, BindingPriority.LocalValue); + var expression = new IndexerBindingExpression(Source, Property, target, targetProperty, Mode); + return new InstancedBinding(expression, Mode, BindingPriority.LocalValue); + } + + IBindingExpression IBinding2.Instance(AvaloniaObject target, AvaloniaProperty targetProperty) + { + return new IndexerBindingExpression(Source, Property, target, targetProperty, Mode); } } } diff --git a/src/Avalonia.Base/Data/TemplateBinding.cs b/src/Avalonia.Base/Data/TemplateBinding.cs index 05d9663ad3..b4561e65f7 100644 --- a/src/Avalonia.Base/Data/TemplateBinding.cs +++ b/src/Avalonia.Base/Data/TemplateBinding.cs @@ -13,6 +13,7 @@ namespace Avalonia.Data /// public partial class TemplateBinding : UntypedBindingExpressionBase, IBinding, + IBinding2, IDescription, ISetterValue, IDisposable @@ -69,32 +70,12 @@ namespace Avalonia.Data object? anchor = null, bool enableDataValidation = false) { - if (Mode is BindingMode.OneTime or BindingMode.OneWayToSource) - throw new NotSupportedException("TemplateBinding does not support OneTime or OneWayToSource bindings."); - - // Usually each `TemplateBinding` will only be instantiated once; in this case we can - // use the `TemplateBinding` object itself as the binding expression in order to save - // allocating a new object. - // - // If the binding appears in a `Setter`, then make a clone and instantiate that because - // because the setter can outlive the control and cause a leak. - if (!_isSetterValue) - { - return new(target, this, Mode, BindingPriority.Template); - } - else - { - var clone = new TemplateBinding - { - Converter = Converter, - ConverterCulture = ConverterCulture, - ConverterParameter = ConverterParameter, - Mode = Mode, - Property = Property, - }; + return new(target, InstanceCore(), Mode, BindingPriority.Template); + } - return clone.Initiate(target, targetProperty, anchor, enableDataValidation); - } + IBindingExpression IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property) + { + return InstanceCore(); } internal override bool WriteValueToSource(object? value) @@ -140,6 +121,36 @@ namespace Avalonia.Data } } + private TemplateBinding InstanceCore() + { + if (Mode is BindingMode.OneTime or BindingMode.OneWayToSource) + throw new NotSupportedException("TemplateBinding does not support OneTime or OneWayToSource bindings."); + + // Usually each `TemplateBinding` will only be instantiated once; in this case we can + // use the `TemplateBinding` object itself as the binding expression in order to save + // allocating a new object. + // + // If the binding appears in a `Setter`, then make a clone and instantiate that because + // because the setter can outlive the control and cause a leak. + if (!_isSetterValue) + { + return this; + } + else + { + var clone = new TemplateBinding + { + Converter = Converter, + ConverterCulture = ConverterCulture, + ConverterParameter = ConverterParameter, + Mode = Mode, + Property = Property, + }; + + return clone; + } + } + private void PublishValue() { if (Mode == BindingMode.OneWayToSource) diff --git a/src/Avalonia.Base/Diagnostics/ObsoletionMessages.cs b/src/Avalonia.Base/Diagnostics/ObsoletionMessages.cs new file mode 100644 index 0000000000..bfada91abe --- /dev/null +++ b/src/Avalonia.Base/Diagnostics/ObsoletionMessages.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Diagnostics; + +internal static class ObsoletionMessages +{ + public const string MayBeRemovedInAvalonia12 = "This API may be removed in Avalonia 12. If you depend on this API, please open an issue with details of your use-case."; +} diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index 9ecbd309ef..c3aa7ac602 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -183,10 +183,7 @@ namespace Avalonia return o.Bind(this, source); } - internal override IDisposable RouteBind( - AvaloniaObject o, - UntypedBindingExpressionBase source, - BindingPriority priority) + internal override IBindingExpression RouteBind(AvaloniaObject o, UntypedBindingExpressionBase source) { return o.GetValueStore().AddBinding(this, source); } diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index 6d3f701b41..7da95a2990 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -43,11 +43,12 @@ namespace Avalonia.PropertyStore ReevaluateEffectiveValues(); } - public IDisposable AddBinding( + public IBindingExpression AddBinding( StyledProperty property, - UntypedBindingExpressionBase source, - BindingPriority priority) + UntypedBindingExpressionBase source) { + var priority = source.Priority; + if (priority == BindingPriority.LocalValue) { DisposeExistingLocalValueBinding(property); @@ -155,7 +156,7 @@ namespace Avalonia.PropertyStore } } - public IDisposable AddBinding(DirectPropertyBase property, UntypedBindingExpressionBase source) + public IBindingExpression AddBinding(DirectPropertyBase property, UntypedBindingExpressionBase source) { DisposeExistingLocalValueBinding(property); _localValueBindings ??= new(); diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs index 4936a404d4..e8ff8f72ed 100644 --- a/src/Avalonia.Base/StyledProperty.cs +++ b/src/Avalonia.Base/StyledProperty.cs @@ -228,12 +228,9 @@ namespace Avalonia return target.Bind(this, source, priority); } - internal override IDisposable RouteBind( - AvaloniaObject o, - UntypedBindingExpressionBase source, - BindingPriority priority) + internal override IBindingExpression RouteBind(AvaloniaObject o, UntypedBindingExpressionBase source) { - return o.GetValueStore().AddBinding(this, source, priority); + return o.GetValueStore().AddBinding(this, source); } [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConversionSupressWarningMessage)] diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs index 9ef6a06ea6..38f532128c 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Data.Core.ExpressionNodes; +using Avalonia.Diagnostics; using Avalonia.Markup.Parsers; using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings; @@ -38,45 +39,30 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions }; } + [ConstructorArgument("path")] + public CompiledBindingPath Path { get; set; } + + public object? Source { get; set; } = AvaloniaProperty.UnsetValue; + + public Type? DataType { get; set; } + + [Obsolete(ObsoletionMessages.MayBeRemovedInAvalonia12)] public override InstancedBinding? Initiate( AvaloniaObject target, AvaloniaProperty? targetProperty, object? anchor = null, bool enableDataValidation = false) { - var nodes = new List(); - - // Build the expression nodes from the binding path. - Path.BuildExpression(nodes, out var isRooted); - - // If the binding isn't rooted (i.e. doesn't have a Source or start with $parent, $self, - // #elementName etc.) then we need to add a data context source node. - if (Source == AvaloniaProperty.UnsetValue && !isRooted) - nodes.Insert(0, ExpressionNodeFactory.CreateDataContext(targetProperty)); - - // If the first node is an ISourceNode then allow it to select the source; otherwise - // use the binding source if specified, falling back to the target. - var source = nodes.Count > 0 && nodes[0] is SourceNode sn - ? sn.SelectSource(Source, target, anchor ?? DefaultAnchor?.Target) - : Source != AvaloniaProperty.UnsetValue? Source : target; - - // Create the binding expression and wrap it in an InstancedBinding. - var expression = new BindingExpression( - source, - nodes, - FallbackValue, - converter: Converter, - converterCulture: ConverterCulture, - converterParameter: ConverterParameter, - enableDataValidation: enableDataValidation, - mode: ResolveBindingMode(target, targetProperty), - stringFormat: StringFormat, - targetNullValue: TargetNullValue, - targetTypeConverter: TargetTypeConverter.GetDefaultConverter()); - + var expression = InstanceCore(target, targetProperty, enableDataValidation); return new InstancedBinding(target, expression, Mode, Priority); } + private protected override IBindingExpression Instance(AvaloniaProperty targetProperty, AvaloniaObject target) + { + var enableDataValidation = targetProperty.GetMetadata(target.GetType()).EnableDataValidation ?? false; + return InstanceCore(target, targetProperty, enableDataValidation); + } + /// /// Hack for TreeDataTemplate to create a binding expression for an item. /// @@ -85,6 +71,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions /// Ideally we'd do this in a more generic way but didn't have time to refactor /// ITreeDataTemplate in time for 11.0. We should revisit this in 12.0. /// + // TODO12: Refactor internal BindingExpression CreateObservableForTreeDataTemplate(object source) { if (Source != AvaloniaProperty.UnsetValue) @@ -106,11 +93,39 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions targetNullValue: TargetNullValue); } - [ConstructorArgument("path")] - public CompiledBindingPath Path { get; set; } + private BindingExpression InstanceCore( + AvaloniaObject target, + AvaloniaProperty? targetProperty, + bool enableDataValidation) + { + var nodes = new List(); - public object? Source { get; set; } = AvaloniaProperty.UnsetValue; + // Build the expression nodes from the binding path. + Path.BuildExpression(nodes, out var isRooted); - public Type? DataType { get; set; } + // If the binding isn't rooted (i.e. doesn't have a Source or start with $parent, $self, + // #elementName etc.) then we need to add a data context source node. + if (Source == AvaloniaProperty.UnsetValue && !isRooted) + nodes.Insert(0, ExpressionNodeFactory.CreateDataContext(targetProperty)); + + // If the first node is an ISourceNode then allow it to select the source; otherwise + // use the binding source if specified, falling back to the target. + var source = nodes.Count > 0 && nodes[0] is SourceNode sn + ? sn.SelectSource(Source, target, DefaultAnchor?.Target) + : Source != AvaloniaProperty.UnsetValue ? Source : target; + + return new BindingExpression( + source, + nodes, + FallbackValue, + converter: Converter, + converterCulture: ConverterCulture, + converterParameter: ConverterParameter, + enableDataValidation: enableDataValidation, + mode: ResolveBindingMode(target, targetProperty), + stringFormat: StringFormat, + targetNullValue: TargetNullValue, + targetTypeConverter: TargetTypeConverter.GetDefaultConverter()); + } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs index cbce88fbea..e26933edcc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs @@ -1,11 +1,6 @@ using System; -using System.Diagnostics.CodeAnalysis; using Avalonia.Controls; using Avalonia.Data; -using Avalonia.Data.Core; -using Avalonia.Logging; -using Avalonia.Markup.Xaml.Converters; -using Avalonia.Media; using Avalonia.Styling; namespace Avalonia.Markup.Xaml.MarkupExtensions diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index 0812a66f5d..4a95b57fce 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -5,6 +5,7 @@ using Avalonia.Controls; using Avalonia.Data.Converters; using Avalonia.Data.Core; using Avalonia.Data.Core.ExpressionNodes; +using Avalonia.Diagnostics; using Avalonia.Markup.Parsers; using Avalonia.Utilities; @@ -59,16 +60,44 @@ namespace Avalonia.Data /// public Func? TypeResolver { get; set; } + [Obsolete(ObsoletionMessages.MayBeRemovedInAvalonia12)] public override InstancedBinding? Initiate( AvaloniaObject target, AvaloniaProperty? targetProperty, object? anchor = null, bool enableDataValidation = false) { + var expression = InstanceCore(targetProperty, target, enableDataValidation); + return new InstancedBinding(target, expression, Mode, Priority); + } + + private protected override IBindingExpression Instance(AvaloniaProperty targetProperty, AvaloniaObject target) + { + var enableDataValidation = targetProperty.GetMetadata(target.GetType()).EnableDataValidation ?? false; + return InstanceCore(targetProperty, target, enableDataValidation); + } + + /// + /// Hack for TreeDataTemplate to create a binding expression for an item. + /// + /// The item. + /// + /// Ideally we'd do this in a more generic way but didn't have time to refactor + /// ITreeDataTemplate in time for 11.0. We should revisit this in 12.0. + /// + // TODO12: Refactor + internal BindingExpression CreateObservableForTreeDataTemplate(object source) + { + if (!string.IsNullOrEmpty(ElementName)) + throw new NotSupportedException("ElementName bindings are not supported in this context."); + if (RelativeSource is not null && RelativeSource.Mode != RelativeSourceMode.DataContext) + throw new NotSupportedException("RelativeSource bindings are not supported in this context."); + if (Source != AvaloniaProperty.UnsetValue) + throw new NotSupportedException("Source bindings are not supported in this context."); + var nodes = new List(); var isRooted = false; - // Build the expression nodes from the binding path. if (!string.IsNullOrEmpty(Path)) { var reader = new CharacterReader(Path.AsSpan()); @@ -81,55 +110,27 @@ namespace Avalonia.Data out isRooted); } - // If the binding isn't rooted (i.e. doesn't have a Source or start with $parent, $self, - // #elementName etc.) then we need to add a source node. The type of source node will - // depend on the ElementName and RelativeSource properties of the binding and if - // neither of those are set will default to a data context node. - if (Source == AvaloniaProperty.UnsetValue && !isRooted && CreateSourceNode(targetProperty) is { } sourceNode) - nodes.Insert(0, sourceNode); - - // If the first node is an ISourceNode then allow it to select the source; otherwise - // use the binding source if specified, falling back to the target. - var source = nodes.Count > 0 && nodes[0] is SourceNode sn ? - sn.SelectSource(Source, target, anchor ?? DefaultAnchor?.Target) : - Source != AvaloniaProperty.UnsetValue ? Source : target; + if (isRooted) + throw new NotSupportedException("Rooted binding paths are not supported in this context."); - // Create the binding expression and wrap it in an InstancedBinding. - var expression = new BindingExpression( + return new BindingExpression( source, nodes, FallbackValue, converter: Converter, - converterCulture: ConverterCulture, converterParameter: ConverterParameter, - enableDataValidation: enableDataValidation, - mode: ResolveBindingMode(target, targetProperty), - stringFormat: StringFormat, - targetNullValue: TargetNullValue, - targetTypeConverter: TargetTypeConverter.GetReflectionConverter()); - return new InstancedBinding(target, expression, Mode, Priority); + targetNullValue: TargetNullValue); } - /// - /// Hack for TreeDataTemplate to create a binding expression for an item. - /// - /// The item. - /// - /// Ideally we'd do this in a more generic way but didn't have time to refactor - /// ITreeDataTemplate in time for 11.0. We should revisit this in 12.0. - /// - internal BindingExpression CreateObservableForTreeDataTemplate(object source) + private UntypedBindingExpressionBase InstanceCore( + AvaloniaProperty? targetProperty, + AvaloniaObject target, + bool enableDataValidation) { - if (!string.IsNullOrEmpty(ElementName)) - throw new NotSupportedException("ElementName bindings are not supported in this context."); - if (RelativeSource is not null && RelativeSource.Mode != RelativeSourceMode.DataContext) - throw new NotSupportedException("RelativeSource bindings are not supported in this context."); - if (Source != AvaloniaProperty.UnsetValue) - throw new NotSupportedException("Source bindings are not supported in this context."); - var nodes = new List(); var isRooted = false; + // Build the expression nodes from the binding path. if (!string.IsNullOrEmpty(Path)) { var reader = new CharacterReader(Path.AsSpan()); @@ -142,16 +143,31 @@ namespace Avalonia.Data out isRooted); } - if (isRooted) - throw new NotSupportedException("Rooted binding paths are not supported in this context."); + // If the binding isn't rooted (i.e. doesn't have a Source or start with $parent, $self, + // #elementName etc.) then we need to add a source node. The type of source node will + // depend on the ElementName and RelativeSource properties of the binding and if + // neither of those are set will default to a data context node. + if (Source == AvaloniaProperty.UnsetValue && !isRooted && CreateSourceNode(targetProperty) is { } sourceNode) + nodes.Insert(0, sourceNode); + + // If the first node is an ISourceNode then allow it to select the source; otherwise + // use the binding source if specified, falling back to the target. + var source = nodes.Count > 0 && nodes[0] is SourceNode sn ? + sn.SelectSource(Source, target, DefaultAnchor?.Target) : + Source != AvaloniaProperty.UnsetValue ? Source : target; return new BindingExpression( source, nodes, FallbackValue, converter: Converter, + converterCulture: ConverterCulture, converterParameter: ConverterParameter, - targetNullValue: TargetNullValue); + enableDataValidation: enableDataValidation, + mode: ResolveBindingMode(target, targetProperty), + stringFormat: StringFormat, + targetNullValue: TargetNullValue, + targetTypeConverter: TargetTypeConverter.GetReflectionConverter()); } private INameScope? GetNameScope() diff --git a/src/Markup/Avalonia.Markup/Data/BindingBase.cs b/src/Markup/Avalonia.Markup/Data/BindingBase.cs index b15e134dd2..3f9e73d549 100644 --- a/src/Markup/Avalonia.Markup/Data/BindingBase.cs +++ b/src/Markup/Avalonia.Markup/Data/BindingBase.cs @@ -5,10 +5,11 @@ using System.Globalization; using Avalonia.Controls; using Avalonia.Data.Converters; using Avalonia.Data.Core; +using Avalonia.Diagnostics; namespace Avalonia.Data { - public abstract class BindingBase : IBinding + public abstract class BindingBase : IBinding, IBinding2 { /// /// Initializes a new instance of the class. @@ -80,12 +81,15 @@ namespace Avalonia.Data /// [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.TypeConversionSupressWarningMessage)] + [Obsolete(ObsoletionMessages.MayBeRemovedInAvalonia12)] public abstract InstancedBinding? Initiate( AvaloniaObject target, AvaloniaProperty? targetProperty, object? anchor = null, bool enableDataValidation = false); + private protected abstract IBindingExpression Instance(AvaloniaProperty targetProperty, AvaloniaObject target); + private protected BindingMode ResolveBindingMode(AvaloniaObject target, AvaloniaProperty? targetProperty) { if (Mode != BindingMode.Default) @@ -94,5 +98,7 @@ namespace Avalonia.Data return BindingMode.OneWay; return targetProperty.GetMetadata(target.GetType()).DefaultBindingMode; } + + IBindingExpression IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property) => Instance(property, target); } } diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index 993f63b4d3..961f5b9cca 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -5,13 +5,14 @@ using System.Linq; using Avalonia.Reactive; using Avalonia.Data.Converters; using Avalonia.Metadata; +using Avalonia.Data.Core; namespace Avalonia.Data { /// /// A XAML binding that calculates an aggregate value from multiple child . /// - public class MultiBinding : IBinding + public class MultiBinding : IBinding2 { /// /// Gets the collection of child bindings. @@ -72,25 +73,7 @@ namespace Avalonia.Data object? anchor = null, bool enableDataValidation = false) { - var targetType = targetProperty?.PropertyType ?? typeof(object); - var converter = Converter; - // We only respect `StringFormat` if the type of the property we're assigning to will - // accept a string. Note that this is slightly different to WPF in that WPF only applies - // `StringFormat` for target type `string` (not `object`). - if (!string.IsNullOrWhiteSpace(StringFormat) && - (targetType == typeof(string) || targetType == typeof(object))) - { - converter = new StringFormatMultiValueConverter(StringFormat!, converter); - } - - var children = Bindings.Select(x => x.Initiate(target, 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); - + var input = InstanceCore(target, targetProperty); var mode = Mode == BindingMode.Default ? targetProperty?.GetMetadata(target.GetType()).DefaultBindingMode : Mode; @@ -106,6 +89,35 @@ namespace Avalonia.Data } } + IBindingExpression IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property) + { + // TODO: Implement MultiBindingExpression instead of wrapping an observable. + var o = InstanceCore(target, property); + return new UntypedObservableBindingExpression(o); + } + + private IObservable InstanceCore(AvaloniaObject target, AvaloniaProperty? targetProperty) + { + var targetType = targetProperty?.PropertyType ?? typeof(object); + var converter = Converter; + // We only respect `StringFormat` if the type of the property we're assigning to will + // accept a string. Note that this is slightly different to WPF in that WPF only applies + // `StringFormat` for target type `string` (not `object`). + if (!string.IsNullOrWhiteSpace(StringFormat) && + (targetType == typeof(string) || targetType == typeof(object))) + { + converter = new StringFormatMultiValueConverter(StringFormat!, converter); + } + + var children = Bindings.Select(x => x.Initiate(target, null)); + + return children.Select(x => x?.Source) + .Where(x => x is not null)! + .CombineLatest() + .Select(x => ConvertValue(x, targetType, converter)) + .Where(x => x != BindingOperations.DoNothing); + } + private object ConvertValue(IList values, Type targetType, IMultiValueConverter? converter) { for (var i = 0; i < values.Count; ++i) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 57f64dcafa..837fdaeac7 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -175,10 +175,7 @@ namespace Avalonia.Base.UnitTests throw new NotImplementedException(); } - internal override IDisposable RouteBind( - AvaloniaObject o, - UntypedBindingExpressionBase source, - BindingPriority priority) + internal override IBindingExpression RouteBind(AvaloniaObject o, UntypedBindingExpressionBase source) { throw new NotImplementedException(); }