From aa5dc3e0f59e673bb089707008ad94c1da807112 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 2 Jul 2022 23:01:37 +0200 Subject: [PATCH 01/11] Added class selector style benchmarks. --- .../Styling/Style_ClassSelector.cs | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 tests/Avalonia.Benchmarks/Styling/Style_ClassSelector.cs diff --git a/tests/Avalonia.Benchmarks/Styling/Style_ClassSelector.cs b/tests/Avalonia.Benchmarks/Styling/Style_ClassSelector.cs new file mode 100644 index 0000000000..f242e95966 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Styling/Style_ClassSelector.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Avalonia.Controls; +using Avalonia.Styling; +using BenchmarkDotNet.Attributes; + +#nullable enable + +namespace Avalonia.Benchmarks.Styling +{ + [MemoryDiagnoser] + public class Style_ClassSelector + { + private Style _style = null!; + + public Style_ClassSelector() + { + RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle); + } + + [GlobalSetup] + public void Setup() + { + _style = new Style(x => x.OfType().Class("foo")) + { + Setters = { new Setter(TestClass.StringProperty, "foo") } + }; + } + + [Benchmark(OperationsPerInvoke = 50)] + public void Apply() + { + var target = new TestClass(); + + target.BeginBatchUpdate(); + + for (var i = 0; i < 50; ++i) + _style.TryAttach(target, null); + + target.EndBatchUpdate(); + } + + [Benchmark(OperationsPerInvoke = 50)] + public void Apply_Toggle() + { + var target = new TestClass(); + + target.BeginBatchUpdate(); + + for (var i = 0; i < 50; ++i) + _style.TryAttach(target, null); + + target.EndBatchUpdate(); + + target.Classes.Add("foo"); + target.Classes.Remove("foo"); + } + + [Benchmark(OperationsPerInvoke = 50)] + public void Apply_Detach() + { + var target = new TestClass(); + + target.BeginBatchUpdate(); + + for (var i = 0; i < 50; ++i) + _style.TryAttach(target, null); + + target.EndBatchUpdate(); + + target.DetachStyles(); + } + + private class TestClass : Control + { + public static readonly StyledProperty StringProperty = + AvaloniaProperty.Register("String"); + public void DetachStyles() => InvalidateStyles(); + } + + private class TestClass2 : Control + { + } + } +} From e63aa46458022a4a8974afe1eb1792ec7d1968c3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 3 Jul 2022 00:14:04 +0200 Subject: [PATCH 02/11] Add a more efficient way to listen to classes changes. Improves the situation in e.g. #8389 drastically. --- src/Avalonia.Base/Controls/Classes.cs | 53 ++++++++++++++++++- .../Controls/IClassesChangedListener.cs | 14 +++++ .../Styling/Activators/StyleClassActivator.cs | 26 ++++----- .../Styling/TypeNameAndClassSelector.cs | 3 +- .../Styling/SelectorTests_Template.cs | 5 +- tests/Avalonia.LeakTests/ControlTests.cs | 2 +- 6 files changed, 80 insertions(+), 23 deletions(-) create mode 100644 src/Avalonia.Base/Controls/IClassesChangedListener.cs diff --git a/src/Avalonia.Base/Controls/Classes.cs b/src/Avalonia.Base/Controls/Classes.cs index 50605661fa..e64209c3cb 100644 --- a/src/Avalonia.Base/Controls/Classes.cs +++ b/src/Avalonia.Base/Controls/Classes.cs @@ -14,6 +14,8 @@ namespace Avalonia.Controls /// public class Classes : AvaloniaList, IPseudoClasses { + private List? _listeners; + /// /// Initializes a new instance of the class. /// @@ -39,6 +41,11 @@ namespace Avalonia.Controls { } + /// + /// Gets the number of listeners subscribed to this collection for unit testing purposes. + /// + internal int ListenerCount => _listeners?.Count ?? 0; + /// /// Parses a classes string. /// @@ -62,6 +69,7 @@ namespace Avalonia.Controls if (!Contains(name)) { base.Add(name); + NotifyChanged(); } } @@ -89,6 +97,7 @@ namespace Avalonia.Controls } base.AddRange(c); + NotifyChanged(); } /// @@ -103,6 +112,8 @@ namespace Avalonia.Controls RemoveAt(i); } } + + NotifyChanged(); } /// @@ -122,6 +133,7 @@ namespace Avalonia.Controls if (!Contains(name)) { base.Insert(index, name); + NotifyChanged(); } } @@ -154,6 +166,7 @@ namespace Avalonia.Controls if (toInsert != null) { base.InsertRange(index, toInsert); + NotifyChanged(); } } @@ -169,7 +182,14 @@ namespace Avalonia.Controls public override bool Remove(string name) { ThrowIfPseudoclass(name, "removed"); - return base.Remove(name); + + if (base.Remove(name)) + { + NotifyChanged(); + return true; + } + + return false; } /// @@ -197,6 +217,7 @@ namespace Avalonia.Controls if (toRemove != null) { base.RemoveAll(toRemove); + NotifyChanged(); } } @@ -214,6 +235,7 @@ namespace Avalonia.Controls var name = this[index]; ThrowIfPseudoclass(name, "removed"); base.RemoveAt(index); + NotifyChanged(); } /// @@ -224,6 +246,7 @@ namespace Avalonia.Controls public override void RemoveRange(int index, int count) { base.RemoveRange(index, count); + NotifyChanged(); } /// @@ -255,6 +278,7 @@ namespace Avalonia.Controls } base.AddRange(source); + NotifyChanged(); } /// @@ -263,13 +287,38 @@ namespace Avalonia.Controls if (!Contains(name)) { base.Add(name); + NotifyChanged(); } } /// bool IPseudoClasses.Remove(string name) { - return base.Remove(name); + if (base.Remove(name)) + { + NotifyChanged(); + return true; + } + + return false; + } + + internal void AddListener(IClassesChangedListener listener) + { + (_listeners ??= new()).Add(listener); + } + + internal void RemoveListener(IClassesChangedListener listener) + { + _listeners?.Remove(listener); + } + + private void NotifyChanged() + { + if (_listeners is null) + return; + foreach (var listener in _listeners) + listener.Changed(); } private void ThrowIfPseudoclass(string name, string operation) diff --git a/src/Avalonia.Base/Controls/IClassesChangedListener.cs b/src/Avalonia.Base/Controls/IClassesChangedListener.cs new file mode 100644 index 0000000000..b4de893c97 --- /dev/null +++ b/src/Avalonia.Base/Controls/IClassesChangedListener.cs @@ -0,0 +1,14 @@ +namespace Avalonia.Controls +{ + /// + /// Internal interface for listening to changes in in a more + /// performant manner than subscribing to CollectionChanged. + /// + internal interface IClassesChangedListener + { + /// + /// Notifies the listener that the collection has changed. + /// + void Changed(); + } +} diff --git a/src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs b/src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs index 98d3f16a0a..3f70ff50b3 100644 --- a/src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs +++ b/src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using Avalonia.Collections; +using Avalonia.Controls; #nullable enable @@ -10,21 +11,17 @@ namespace Avalonia.Styling.Activators /// An which is active when a set of classes match those on a /// control. /// - internal sealed class StyleClassActivator : StyleActivatorBase + internal sealed class StyleClassActivator : StyleActivatorBase, IClassesChangedListener { private readonly IList _match; - private readonly IAvaloniaReadOnlyList _classes; - private NotifyCollectionChangedEventHandler? _classesChangedHandler; + private readonly Classes _classes; - public StyleClassActivator(IAvaloniaReadOnlyList classes, IList match) + public StyleClassActivator(Classes classes, IList match) { _classes = classes; _match = match; } - private NotifyCollectionChangedEventHandler ClassesChangedHandler => - _classesChangedHandler ??= ClassesChanged; - public static bool AreClassesMatching(IReadOnlyList classes, IList toMatch) { int remainingMatches = toMatch.Count; @@ -55,23 +52,20 @@ namespace Avalonia.Styling.Activators return remainingMatches == 0; } - protected override void Initialize() + void IClassesChangedListener.Changed() { PublishNext(IsMatching()); - _classes.CollectionChanged += ClassesChangedHandler; } - protected override void Deinitialize() + protected override void Initialize() { - _classes.CollectionChanged -= ClassesChangedHandler; + PublishNext(IsMatching()); + _classes.AddListener(this); } - private void ClassesChanged(object? sender, NotifyCollectionChangedEventArgs e) + protected override void Deinitialize() { - if (e.Action != NotifyCollectionChangedAction.Move) - { - PublishNext(IsMatching()); - } + _classes.RemoveListener(this); } private bool IsMatching() => AreClassesMatching(_classes, _match); diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs index 24d5d6bbbf..d52c8c7d5c 100644 --- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using Avalonia.Controls; using Avalonia.Styling.Activators; #nullable enable @@ -125,7 +126,7 @@ namespace Avalonia.Styling { if (subscribe) { - var observable = new StyleClassActivator(control.Classes, _classes.Value); + var observable = new StyleClassActivator((Classes)control.Classes, _classes.Value); return new SelectorMatch(observable); } diff --git a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs index 8b4c988037..176fa07f19 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs @@ -142,14 +142,13 @@ namespace Avalonia.Base.UnitTests.Styling var border = (Border)target.Object.VisualChildren.Single(); var selector = default(Selector).OfType(templatedControl.Object.GetType()).Class("foo").Template().OfType(); var activator = selector.Match(border).Activator; - var inccDebug = (INotifyCollectionChangedDebug)styleable.Object.Classes; using (activator.Subscribe(_ => { })) { - Assert.Single(inccDebug.GetCollectionChangedSubscribers()); + Assert.Equal(1, ((Classes)styleable.Object.Classes).ListenerCount); } - Assert.Null(inccDebug.GetCollectionChangedSubscribers()); + Assert.Equal(0, ((Classes)styleable.Object.Classes).ListenerCount); } private void BuildVisualTree(Mock templatedControl) where T : class, IVisual diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 8c05f2a0a7..8651409af1 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -313,7 +313,7 @@ namespace Avalonia.LeakTests // The TextBox should have subscriptions to its Classes collection from the // default theme. - Assert.NotEmpty(((INotifyCollectionChangedDebug)textBox.Classes).GetCollectionChangedSubscribers()); + Assert.NotEqual(0, textBox.Classes.ListenerCount); // Clear the content and ensure the TextBox is removed. window.Content = null; From f80cf2d355f813d9294523984c6323398b4ef65d Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Thu, 7 Jul 2022 23:01:38 +0300 Subject: [PATCH 03/11] add base.OnAttachedToVisualTree(e) call to repeater --- src/Avalonia.Controls/Repeater/ItemsRepeater.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 3f42d95deb..ae663defd3 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -421,6 +421,7 @@ namespace Avalonia.Controls protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { + base.OnAttachedToVisualTree(e); InvalidateMeasure(); _viewportManager.ResetScrollers(); } From a1804e2e16611556e169243dff73e847935bf011 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 8 Jul 2022 01:02:01 -0400 Subject: [PATCH 04/11] Add tests for explicitly implemented interface property binding --- .../CompiledBindingExtensionTests.cs | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index f3f2d2f1e4..898641f2ee 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -1145,6 +1145,28 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Equal("bar-" + typeof(TestDataContext).FullName, textBlock.Text); } } + + [Fact] + public void SupportCastToTypeInExpressionWithProperty_ExplicitPropertyCast() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var contentControl = window.GetControl("contentControl"); + + var dataContext = new TestDataContext(); + + window.DataContext = dataContext; + + Assert.Equal(((IHasExplicitProperty)dataContext).ExplicitProperty, contentControl.Content); + } + } [Fact] public void Binds_To_Self_Without_DataType() @@ -1410,6 +1432,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions public interface IHasPropertyDerived : IHasProperty { } + public interface IHasExplicitProperty + { + string ExplicitProperty { get; } + } + public class AppendConverter : IValueConverter { public static IValueConverter Instance { get; } = new AppendConverter(); @@ -1429,7 +1456,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions public class TestDataContextBaseClass {} - public class TestDataContext : TestDataContextBaseClass, IHasPropertyDerived + public class TestDataContext : TestDataContextBaseClass, IHasPropertyDerived, IHasExplicitProperty { public string StringProperty { get; set; } @@ -1449,6 +1476,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions public INonIntegerIndexerDerived NonIntegerIndexerInterfaceProperty => NonIntegerIndexerProperty; + string IHasExplicitProperty.ExplicitProperty => "Hello"; + + public string ExplicitProperty => "Bye"; + public class NonIntegerIndexer : NotifyingBase, INonIntegerIndexerDerived { private readonly Dictionary _storage = new Dictionary(); From 042fd47c1e08fdd33023176520cf7c10f07b1ffd Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 8 Jul 2022 01:04:00 -0400 Subject: [PATCH 05/11] Fix ElementName binding inside of control template --- .../XamlIlBindingPathHelper.cs | 7 ++++ .../CompiledBindingExtensionTests.cs | 32 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs index c8de8f00f6..dd809956cd 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -374,6 +374,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions public static IXamlType GetTargetType(IXamlAstNode namescopeRoot, string name) { + // If we start from the nested scope - skip it. + if (namescopeRoot is NestedScopeMetadataNode scope) + { + namescopeRoot = scope.Value; + } + var finder = new ScopeRegistrationFinder(name); namescopeRoot.Visit(finder); return finder.TargetType; @@ -399,6 +405,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) { + // Ignore name registrations, if we are inside of the nested namescope. if (_childScopesStack.Count == 0 && node is AvaloniaNameScopeRegistrationXamlIlNode registration) { if (registration.Name is XamlAstTextNode text && text.Text == Name) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 898641f2ee..e6df80f17e 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -641,6 +641,38 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + + [Fact] + public void ResolvesElementNameInTemplate() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + +"; + + var contentControl = AvaloniaRuntimeXamlLoader.Parse(xaml); + contentControl.Measure(new Size(10, 10)); + + var result = contentControl.GetTemplateChildren().OfType().First(); + + Assert.Equal("Hello", result.Content); + } + } + [Fact] public void Binds_To_Source() { From 142b878984db2f3be0977b4073a83ff3247ab2f0 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 8 Jul 2022 01:05:25 -0400 Subject: [PATCH 06/11] Fix special case of compiled binding with Path child --- .../XamlIlBindingPathHelper.cs | 8 +++-- .../CompiledBindingExtensionTests.cs | 35 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs index dd809956cd..8a115bf8fc 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -47,7 +47,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions return startTypeResolver(); } - if (bindingPathAssignment.Values[0] is ParsedBindingPathNode bindingPathNode) + if (bindingPathAssignment.Values[0] is XamlIlBindingPathNode pathNode) + { + bindingResultType = pathNode.BindingResultType; + } + else if (bindingPathAssignment.Values[0] is ParsedBindingPathNode bindingPathNode) { var transformed = TransformBindingPath( context, @@ -63,7 +67,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } else { - throw new InvalidOperationException(); + throw new InvalidOperationException("Invalid state of Path property"); } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index e6df80f17e..754a819182 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -102,6 +102,41 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Equal(dataContext.StringProperty, textBlock.Text); } } + + [Fact] + public void ResolvesPathPassedByPropertyWithInnerItemTemplate() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("itemsControl"); + + var dataContext = new TestDataContext + { + ListProperty = + { + "Hello" + } + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.ListProperty, textBlock.Items); + } + } [Fact] public void ResolvesStreamTaskBindingCorrectly() From 2619ab17595b6317fe4afdf1bc0917cb325c5a1d Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 8 Jul 2022 01:06:13 -0400 Subject: [PATCH 07/11] Fix compiled binding with TemplatedParent relative source --- .../AvaloniaXamlIlCompiler.cs | 6 ++-- .../AvaloniaXamlIlBindingPathParser.cs | 25 ++++++++++------ .../XamlIlBindingPathHelper.cs | 1 + .../CompiledBindingExtensionTests.cs | 30 +++++++++++++++++-- 4 files changed, 47 insertions(+), 15 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 04a61e5f10..e3a55feac9 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -45,10 +45,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions new AvaloniaXamlIlReorderClassesPropertiesTransformer() ); - InsertBefore( - new AvaloniaXamlIlBindingPathParser(), + InsertBefore( new AvaloniaXamlIlSelectorTransformer(), - new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), + new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), + new AvaloniaXamlIlBindingPathParser(), new AvaloniaXamlIlPropertyPathTransformer(), new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlConstructorServiceProviderTransformer(), diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs index 890cbb69bf..60c880c6aa 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs @@ -121,11 +121,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { throw new XamlParseException("Only one of ElementName, Source, or RelativeSource specified as a binding source. Only one property is allowed.", binding); } - - var mode = relativeSourceObject.Children + + var modeProperty = relativeSourceObject.Children .OfType() - .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Mode") - ?.Values[0] is XamlAstTextNode modeAssignedValue ? modeAssignedValue.Text : null; + .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Mode")? + .Values.FirstOrDefault() as XamlAstTextNode + ?? relativeSourceObject.Arguments.OfType().FirstOrDefault(); + + var mode = modeProperty?.Text; if (relativeSourceObject.Arguments.Count == 0 && mode == null) { mode = "FindAncestor"; @@ -212,16 +215,20 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } else if (mode == "TemplatedParent") { - var parentType = context.ParentNodes().OfType() + var contentTemplateNode = context.ParentNodes().OfType() .FirstOrDefault(x => - x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate) - ?.TargetType.GetClrType(); - - if (parentType is null) + x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate); + if (contentTemplateNode is null) { throw new XamlParseException("A binding with a TemplatedParent RelativeSource has to be in a ControlTemplate.", binding); } + var parentType = contentTemplateNode.TargetType.GetClrType(); + if (parentType is null) + { + throw new XamlParseException("TargetType has to be set on ControlTemplate.", binding); + } + convertedNode = new TemplatedParentBindingExpressionNode { Type = parentType }; } else diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs index 8a115bf8fc..ae29dcf9cb 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -244,6 +244,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions case TemplatedParentBindingExpressionNode templatedParent: var templatedParentField = context.GetAvaloniaTypes().StyledElement.GetAllFields() .FirstOrDefault(f => f.IsStatic && f.IsPublic && f.Name == "TemplatedParentProperty"); + nodes.Add(new SelfPathElementNode(selfType)); nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode( templatedParentField, templatedParent.Type)); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 754a819182..5ca25a176c 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Globalization; +using System.Linq; using System.Reactive.Subjects; -using System.Text; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Presenters; @@ -17,8 +17,6 @@ using Avalonia.Markup.Xaml.Templates; using Avalonia.Media; using Avalonia.Metadata; using Avalonia.UnitTests; -using JetBrains.Annotations; -using XamlX; using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions @@ -676,6 +674,32 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void ResolvesRelativeSourceBindingFromTemplate() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + +"; + + var contentControl = AvaloniaRuntimeXamlLoader.Parse(xaml); + contentControl.Measure(new Size(10, 10)); + + var result = contentControl.GetTemplateChildren().OfType().First(); + Assert.Equal("Hello", result.Content); + } + } [Fact] public void ResolvesElementNameInTemplate() From 5d234cd49c7e7da9c446d7450f06267053a71999 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 8 Jul 2022 01:06:30 -0400 Subject: [PATCH 08/11] Remove reflection bindings from standard controls --- .../Diagnostics/Controls/FilterTextBox.axaml | 2 +- src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml | 2 +- src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml index b7995c38e3..a1a2ab34bf 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml @@ -19,7 +19,7 @@ Classes="textBoxClearButton" ToolTip.Tip="Clear" Cursor="Hand" - Command="{ReflectionBinding $parent[TextBox].Clear}" + Command="{Binding $parent[TextBox].Clear}" Opacity="0.5" /> diff --git a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml index a463334a76..482b219589 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml @@ -21,7 +21,7 @@ - + @@ -32,7 +32,7 @@ - + From ff3b23b2d6eb3e874ad2c71393181b7756ffb5ae Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 8 Jul 2022 01:17:05 -0400 Subject: [PATCH 09/11] Formatting --- .../Transformers/AvaloniaXamlIlBindingPathParser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs index 60c880c6aa..6150265cf8 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs @@ -121,7 +121,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { throw new XamlParseException("Only one of ElementName, Source, or RelativeSource specified as a binding source. Only one property is allowed.", binding); } - + var modeProperty = relativeSourceObject.Children .OfType() .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Mode")? @@ -226,7 +226,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers var parentType = contentTemplateNode.TargetType.GetClrType(); if (parentType is null) { - throw new XamlParseException("TargetType has to be set on ControlTemplate.", binding); + throw new XamlParseException("TargetType has to be set on ControlTemplate or it should be defined inside of a Style.", binding); } convertedNode = new TemplatedParentBindingExpressionNode { Type = parentType }; From 34f8f3d5564d3d7b1584746c8dcaa887a5e235f5 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 8 Jul 2022 01:57:06 -0400 Subject: [PATCH 10/11] Fix DataType set on Binding extension + add tests --- .../AvaloniaXamlIlBindingPathTransformer.cs | 5 + .../CompiledBindings/CompiledBindingPath.cs | 2 + .../CompiledBindingExtensionTests.cs | 95 +++++++++++++++++++ 3 files changed, 102 insertions(+) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs index 3cc3504e16..e509d47932 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs @@ -105,6 +105,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { startType = TypeReferenceResolver.ResolveType(context, text.Text, isMarkupExtension: false, text, strict: true).Type; } + + if (dataTypeProperty?.Values.Count is 1 && dataTypeProperty.Values[0] is XamlTypeExtensionNode typeNode) + { + startType = typeNode.Value.GetClrType(); + } Func startTypeResolver = startType is not null ? () => startType : () => { diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs index 73a14fd437..f78d4867ff 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs @@ -74,6 +74,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings return pathRoot ?? new EmptyExpressionNode(); } + internal IEnumerable Elements => _elements; + internal SourceMode SourceMode => _elements.Count > 0 && _elements[0] is IControlSourceBindingPathElement ? SourceMode.Control : SourceMode.Data; internal object RawSource { get; } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 5ca25a176c..215ae4d54f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -9,10 +9,13 @@ using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Data.Converters; using Avalonia.Data.Core; using Avalonia.Input; using Avalonia.Markup.Data; +using Avalonia.Markup.Xaml.MarkupExtensions; +using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings; using Avalonia.Markup.Xaml.Templates; using Avalonia.Media; using Avalonia.Metadata; @@ -135,6 +138,56 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Equal(dataContext.ListProperty, textBlock.Items); } } + + [Fact] + public void ResolvesDataTypeFromBindingProperty() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, textBlock.Text); + } + } + + [Fact] + public void ResolvesDataTypeFromBindingProperty_TypeExtension() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, textBlock.Text); + } + } [Fact] public void ResolvesStreamTaskBindingCorrectly() @@ -1479,6 +1532,43 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void ResolvesDataTypeForAssignBinding() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" +"; + var control = (AssignBindingControl)AvaloniaRuntimeXamlLoader.Load(xaml); + var compiledPath = ((CompiledBindingExtension)control.X).Path; + + var node = Assert.IsType(Assert.Single(compiledPath.Elements)); + Assert.Equal(typeof(string), node.Property.PropertyType); + } + } + + [Fact] + public void ResolvesDataTypeForAssignBinding_FromBindingProperty() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" +"; + var control = (AssignBindingControl)AvaloniaRuntimeXamlLoader.Load(xaml); + var compiledPath = ((CompiledBindingExtension)control.X).Path; + + var node = Assert.IsType(Assert.Single(compiledPath.Elements)); + Assert.Equal(typeof(string), node.Property.PropertyType); + } + } + void Throws(string type, Action cb) { try @@ -1656,4 +1746,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } public class CustomDataTemplateInherit : CustomDataTemplate { } + + public class AssignBindingControl : Control + { + [AssignBinding] public IBinding X { get; set; } + } } From 8e57d0b7c6b52e6598570795e206a4a27e7e13b9 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Fri, 8 Jul 2022 11:52:55 +0300 Subject: [PATCH 11/11] Fix bindings to Types. --- .../Data/Core/Plugins/InpcPropertyAccessorPlugin.cs | 2 +- tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index b93bf87fdf..91d69b5d3d 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -55,7 +55,7 @@ namespace Avalonia.Data.Core.Plugins private PropertyInfo? GetFirstPropertyWithName(object instance, string propertyName) { - if (instance is IReflectableType reflectableType) + if (instance is IReflectableType reflectableType && instance is not Type) return reflectableType.GetTypeInfo().GetProperty(propertyName, PropertyBindingFlags); var type = instance.GetType(); diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index 11a22f0dec..f4a4a2934f 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -637,6 +637,17 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal("baz", source["Foo"]); } + [Fact] + public void Binding_To_Types_Should_Work() + { + var type = typeof(string); + var textBlock = new TextBlock() { DataContext = type }; + using (textBlock.Bind(TextBlock.TextProperty, new Binding("Name"))) + { + Assert.Equal("String", textBlock.Text); + }; + } + private class StyledPropertyClass : AvaloniaObject { public static readonly StyledProperty DoubleValueProperty =