From 6bd0d2818c5e0c759d3a2729566ad2c0fced2689 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 29 Jun 2022 13:40:10 +0200 Subject: [PATCH 01/20] Add deferred support to ResourceDictionary. --- .../Controls/ResourceDictionary.cs | 40 +++++++++++++++---- .../Controls/Templates/ITemplateResult.cs | 8 ++++ .../Controls}/Templates/TemplateResult.cs | 3 +- 3 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 src/Avalonia.Base/Controls/Templates/ITemplateResult.cs rename src/{Avalonia.Controls => Avalonia.Base/Controls}/Templates/TemplateResult.cs (80%) diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index 77863e5101..e3a8bbbcbc 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; using Avalonia.Collections; +using Avalonia.Controls.Templates; namespace Avalonia.Controls { @@ -29,7 +30,11 @@ namespace Avalonia.Controls public object? this[object key] { - get => _inner?[key]; + get + { + TryGetValue(key, out var value); + return value; + } set { Inner[key] = value; @@ -119,6 +124,12 @@ namespace Avalonia.Controls Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); } + public void AddDeferred(object key, Func factory) + { + Inner.Add(key, new DeferredItem(factory)); + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } + public void Clear() { if (_inner?.Count > 0) @@ -143,10 +154,8 @@ namespace Avalonia.Controls public bool TryGetResource(object key, out object? value) { - if (_inner is not null && _inner.TryGetValue(key, out value)) - { + if (TryGetValue(key, out value)) return true; - } if (_mergedDictionaries != null) { @@ -165,13 +174,24 @@ namespace Avalonia.Controls public bool TryGetValue(object key, out object? value) { - if (_inner is not null) - return _inner.TryGetValue(key, out value); + if (_inner is not null && _inner.TryGetValue(key, out value)) + { + if (value is DeferredItem deffered) + { + _inner[key] = value = deffered.Factory(null) switch + { + ITemplateResult t => t.Result, + object v => v, + _ => null, + }; + } + return true; + } + value = null; return false; } - void ICollection>.Add(KeyValuePair item) { Add(item.Key, item.Value); @@ -258,5 +278,11 @@ namespace Avalonia.Controls } } } + + private class DeferredItem + { + public DeferredItem(Func factory) => Factory = factory; + public Func Factory { get; } + } } } diff --git a/src/Avalonia.Base/Controls/Templates/ITemplateResult.cs b/src/Avalonia.Base/Controls/Templates/ITemplateResult.cs new file mode 100644 index 0000000000..6bd4d735a7 --- /dev/null +++ b/src/Avalonia.Base/Controls/Templates/ITemplateResult.cs @@ -0,0 +1,8 @@ +namespace Avalonia.Controls.Templates +{ + public interface ITemplateResult + { + public object? Result { get; } + public INameScope NameScope { get; } + } +} diff --git a/src/Avalonia.Controls/Templates/TemplateResult.cs b/src/Avalonia.Base/Controls/Templates/TemplateResult.cs similarity index 80% rename from src/Avalonia.Controls/Templates/TemplateResult.cs rename to src/Avalonia.Base/Controls/Templates/TemplateResult.cs index 770aecc329..0e38c6c0ce 100644 --- a/src/Avalonia.Controls/Templates/TemplateResult.cs +++ b/src/Avalonia.Base/Controls/Templates/TemplateResult.cs @@ -1,9 +1,10 @@ namespace Avalonia.Controls.Templates { - public class TemplateResult + public class TemplateResult : ITemplateResult { public T Result { get; } public INameScope NameScope { get; } + object? ITemplateResult.Result => Result; public TemplateResult(T result, INameScope nameScope) { From 5c6eeed4fae39ec31824c9dedb94183aee143e5d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 29 Jun 2022 14:01:41 +0200 Subject: [PATCH 02/20] Initial support for deferred resources in XAML compiler. --- .../Controls/ResourceDictionary.cs | 18 +++- .../AvaloniaXamlIlCompiler.cs | 4 + ...aloniaXamlIlDeferredResourceTransformer.cs | 80 +++++++++++++++ .../AvaloniaXamlIlWellKnownTypes.cs | 9 ++ .../Xaml/ResourceDictionaryTests.cs | 98 +++++++++++++++++++ 5 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index e3a8bbbcbc..d6197c50c6 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -192,6 +192,11 @@ namespace Avalonia.Controls return false; } + public IEnumerator> GetEnumerator() + { + return _inner?.GetEnumerator() ?? Enumerable.Empty>().GetEnumerator(); + } + void ICollection>.Add(KeyValuePair item) { Add(item.Key, item.Value); @@ -218,12 +223,17 @@ namespace Avalonia.Controls return false; } - public IEnumerator> GetEnumerator() + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + internal bool ContainsDeferredKey(object key) { - return _inner?.GetEnumerator() ?? Enumerable.Empty>().GetEnumerator(); - } + if (_inner is not null && _inner.TryGetValue(key, out var result)) + { + return result is DeferredItem; + } - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + return false; + } void IResourceProvider.AddOwner(IResourceHost owner) { diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 04a61e5f10..fac053df14 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -59,6 +59,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions InsertAfter( new XDataTypeTransformer()); + InsertBefore( + new AvaloniaXamlIlDeferredResourceTransformer() + ); + // After everything else InsertBefore( new AddNameScopeRegistration(), diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs new file mode 100644 index 0000000000..099878df08 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Linq; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + internal class AvaloniaXamlIlDeferredResourceTransformer : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (!(node is XamlPropertyAssignmentNode pa) || pa.Values.Count != 2) + return node; + + var types = context.GetAvaloniaTypes(); + + if (pa.Property.DeclaringType == types.ResourceDictionary && pa.Property.Name == "Content") + { + pa.Values[1] = new XamlDeferredContentNode(pa.Values[1], types.XamlIlTypes.Object, context.Configuration); + pa.PossibleSetters = new List + { + new XamlDirectCallPropertySetter(types.ResourceDictionaryDeferredAdd), + }; + } + else if (pa.Property.Name == "Resources" && pa.Property.Getter.ReturnType.Equals(types.IResourceDictionary)) + { + pa.Values[1] = new XamlDeferredContentNode(pa.Values[1], types.XamlIlTypes.Object, context.Configuration); + pa.PossibleSetters = new List + { + new AdderSetter(pa.Property.Getter, types.ResourceDictionaryDeferredAdd), + }; + } + + return node; + } + + class AdderSetter : IXamlPropertySetter, IXamlEmitablePropertySetter + { + private readonly IXamlMethod _getter; + private readonly IXamlMethod _adder; + + public AdderSetter(IXamlMethod getter, IXamlMethod adder) + { + _getter = getter; + _adder = adder; + TargetType = getter.DeclaringType; + Parameters = adder.ParametersWithThis().Skip(1).ToList(); + } + + public IXamlType TargetType { get; } + + public PropertySetterBinderParameters BinderParameters { get; } = new PropertySetterBinderParameters + { + AllowMultiple = true + }; + + public IReadOnlyList Parameters { get; } + public void Emit(IXamlILEmitter emitter) + { + var locals = new Stack(); + // Save all "setter" parameters + for (var c = Parameters.Count - 1; c >= 0; c--) + { + var loc = emitter.LocalsPool.GetLocal(Parameters[c]); + locals.Push(loc); + emitter.Stloc(loc.Local); + } + + emitter.EmitCall(_getter); + while (locals.Count > 0) + using (var loc = locals.Pop()) + emitter.Ldloc(loc.Local); + emitter.EmitCall(_adder, true); + } + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 28787d9b84..330d836f49 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -96,6 +96,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType TextDecorations { get; } public IXamlType TextTrimming { get; } public IXamlType ISetter { get; } + public IXamlType IResourceDictionary { get; } + public IXamlType ResourceDictionary { get; } + public IXamlMethod ResourceDictionaryDeferredAdd { get; } public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg) { @@ -210,6 +213,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers TextDecorations = cfg.TypeSystem.GetType("Avalonia.Media.TextDecorations"); TextTrimming = cfg.TypeSystem.GetType("Avalonia.Media.TextTrimming"); ISetter = cfg.TypeSystem.GetType("Avalonia.Styling.ISetter"); + IResourceDictionary = cfg.TypeSystem.GetType("Avalonia.Controls.IResourceDictionary"); + ResourceDictionary = cfg.TypeSystem.GetType("Avalonia.Controls.ResourceDictionary"); + ResourceDictionaryDeferredAdd = ResourceDictionary.FindMethod("AddDeferred", XamlIlTypes.Void, true, XamlIlTypes.Object, + cfg.TypeSystem.GetType("System.Func`2").MakeGenericType( + cfg.TypeSystem.GetType("System.IServiceProvider"), + XamlIlTypes.Object)); } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs index 578fa888a3..939c5319e4 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs @@ -66,6 +66,104 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [Fact] + public void Item_Is_Added_To_ResourceDictionary_As_Deferred() + { + using (StyledWindow()) + { + var xaml = @" + + Red +"; + var resources = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(xaml); + + Assert.True(resources.ContainsDeferredKey("Red")); + } + } + + [Fact] + public void Item_Is_Added_To_Window_Resources_As_Deferred() + { + using (StyledWindow()) + { + var xaml = @" + + + Red + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var resources = (ResourceDictionary)window.Resources; + + Assert.True(resources.ContainsDeferredKey("Red")); + } + } + + [Fact] + public void Item_Is_Added_To_Window_MergedDictionaries_As_Deferred() + { + using (StyledWindow()) + { + var xaml = @" + + + + + + Red + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var resources = (ResourceDictionary)window.Resources.MergedDictionaries[0]; + + Assert.True(resources.ContainsDeferredKey("Red")); + } + } + + [Fact] + public void Item_Is_Added_To_Style_Resources_As_Deferred() + { + using (StyledWindow()) + { + var xaml = @" +"; + var style = (Style)AvaloniaRuntimeXamlLoader.Load(xaml); + var resources = (ResourceDictionary)style.Resources; + + Assert.True(resources.ContainsDeferredKey("Red")); + } + } + + [Fact] + public void Item_Is_Added_To_Styles_Resources_As_Deferred() + { + using (StyledWindow()) + { + var xaml = @" + + + Red + +"; + var style = (Styles)AvaloniaRuntimeXamlLoader.Load(xaml); + var resources = (ResourceDictionary)style.Resources; + + Assert.True(resources.ContainsDeferredKey("Red")); + } + } + private IDisposable StyledWindow(params (string, string)[] assets) { var services = TestServices.StyledWindow.With( From ccdc11d0accad930a11996546d4cef9af41d191b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Jun 2022 15:34:48 +0200 Subject: [PATCH 03/20] Fix missing subscribe if owner is already set. --- src/Avalonia.Base/Controls/ResourceNodeExtensions.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs index 1758c45650..6121646107 100644 --- a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs @@ -132,6 +132,11 @@ namespace Avalonia.Controls { _target.OwnerChanged += OwnerChanged; _owner = _target.Owner; + + if (_owner is object) + { + _owner.ResourcesChanged += ResourcesChanged; + } } protected override void Deinitialize() From b3bd2e54ecb5fe274164f9ddd96be7482b4a1173 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Mon, 1 Aug 2022 16:19:41 +0300 Subject: [PATCH 04/20] Use correct ToggleModifier in ListBox multiselection on MacOS. --- src/Avalonia.Controls/ListBox.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 79285bb86b..80b5259a53 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -6,6 +6,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Selection; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -157,7 +158,7 @@ namespace Avalonia.Controls e.Source, true, e.KeyModifiers.HasAllFlags(KeyModifiers.Shift), - e.KeyModifiers.HasAllFlags(KeyModifiers.Control), + e.KeyModifiers.HasAllFlags(AvaloniaLocator.Current.GetRequiredService().CommandModifiers), point.Properties.IsRightButtonPressed); } } From fe79cee6726a76e875a3184218106d7b3b6dc6de Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Tue, 2 Aug 2022 16:11:08 +0300 Subject: [PATCH 05/20] Fix Tests. --- .../ListBoxTests_Single.cs | 133 +++-- .../Primitives/SelectingItemsControlTests.cs | 89 +-- .../SelectingItemsControlTests_Multiple.cs | 542 ++++++++++-------- 3 files changed, 422 insertions(+), 342 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs index 8f795104bf..bf516748cc 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs @@ -5,10 +5,12 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.LogicalTree; using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; +using Moq; using Xunit; namespace Avalonia.Controls.UnitTests @@ -60,104 +62,123 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Clicking_Item_Should_Select_It() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = new FuncControlTemplate(CreateListBoxTemplate), - Items = new[] { "Foo", "Bar", "Baz " }, - }; - - ApplyTemplate(target); - _mouse.Click(target.Presenter.Panel.Children[0]); - - Assert.Equal(0, target.SelectedIndex); + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + Items = new[] { "Foo", "Bar", "Baz " }, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + ApplyTemplate(target); + _mouse.Click(target.Presenter.Panel.Children[0]); + + Assert.Equal(0, target.SelectedIndex); + } } [Fact] public void Clicking_Selected_Item_Should_Not_Deselect_It() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = new FuncControlTemplate(CreateListBoxTemplate), - Items = new[] { "Foo", "Bar", "Baz " }, - }; - - ApplyTemplate(target); - target.SelectedIndex = 0; + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + Items = new[] { "Foo", "Bar", "Baz " }, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + ApplyTemplate(target); + target.SelectedIndex = 0; - _mouse.Click(target.Presenter.Panel.Children[0]); + _mouse.Click(target.Presenter.Panel.Children[0]); - Assert.Equal(0, target.SelectedIndex); + Assert.Equal(0, target.SelectedIndex); + } } [Fact] public void Clicking_Item_Should_Select_It_When_SelectionMode_Toggle() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = new FuncControlTemplate(CreateListBoxTemplate), - Items = new[] { "Foo", "Bar", "Baz " }, - SelectionMode = SelectionMode.Single | SelectionMode.Toggle, - }; - - ApplyTemplate(target); + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + Items = new[] { "Foo", "Bar", "Baz " }, + SelectionMode = SelectionMode.Single | SelectionMode.Toggle, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + ApplyTemplate(target); - _mouse.Click(target.Presenter.Panel.Children[0]); + _mouse.Click(target.Presenter.Panel.Children[0]); - Assert.Equal(0, target.SelectedIndex); + Assert.Equal(0, target.SelectedIndex); + } } [Fact] public void Clicking_Selected_Item_Should_Deselect_It_When_SelectionMode_Toggle() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = new FuncControlTemplate(CreateListBoxTemplate), - Items = new[] { "Foo", "Bar", "Baz " }, - SelectionMode = SelectionMode.Toggle, - }; + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + Items = new[] { "Foo", "Bar", "Baz " }, + SelectionMode = SelectionMode.Toggle, + }; - ApplyTemplate(target); - target.SelectedIndex = 0; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + ApplyTemplate(target); + target.SelectedIndex = 0; - _mouse.Click(target.Presenter.Panel.Children[0]); + _mouse.Click(target.Presenter.Panel.Children[0]); - Assert.Equal(-1, target.SelectedIndex); + Assert.Equal(-1, target.SelectedIndex); + } } [Fact] public void Clicking_Selected_Item_Should_Not_Deselect_It_When_SelectionMode_ToggleAlwaysSelected() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = new FuncControlTemplate(CreateListBoxTemplate), - Items = new[] { "Foo", "Bar", "Baz " }, - SelectionMode = SelectionMode.Toggle | SelectionMode.AlwaysSelected, - }; - - ApplyTemplate(target); - target.SelectedIndex = 0; + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + Items = new[] { "Foo", "Bar", "Baz " }, + SelectionMode = SelectionMode.Toggle | SelectionMode.AlwaysSelected, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + ApplyTemplate(target); + target.SelectedIndex = 0; - _mouse.Click(target.Presenter.Panel.Children[0]); + _mouse.Click(target.Presenter.Panel.Children[0]); - Assert.Equal(0, target.SelectedIndex); + Assert.Equal(0, target.SelectedIndex); + } } [Fact] public void Clicking_Another_Item_Should_Select_It_When_SelectionMode_Toggle() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = new FuncControlTemplate(CreateListBoxTemplate), - Items = new[] { "Foo", "Bar", "Baz " }, - SelectionMode = SelectionMode.Single | SelectionMode.Toggle, - }; - - ApplyTemplate(target); - target.SelectedIndex = 1; + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + Items = new[] { "Foo", "Bar", "Baz " }, + SelectionMode = SelectionMode.Single | SelectionMode.Toggle, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + ApplyTemplate(target); + target.SelectedIndex = 1; - _mouse.Click(target.Presenter.Panel.Children[0]); + _mouse.Click(target.Presenter.Panel.Children[0]); - Assert.Equal(0, target.SelectedIndex); + Assert.Equal(0, target.SelectedIndex); + } } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 4b6b6a1182..330cbfd7b9 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -14,6 +14,7 @@ using Avalonia.Controls.Selection; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.Markup.Data; using Avalonia.Platform; @@ -1115,42 +1116,48 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Setting_SelectedItem_With_Pointer_Should_Set_TabOnceActiveElement() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz " }, - }; - - Prepare(target); - _helper.Down((Interactive)target.Presenter.Panel.Children[1]); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz " }, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + Prepare(target); + _helper.Down((Interactive)target.Presenter.Panel.Children[1]); - var panel = target.Presenter.Panel; + var panel = target.Presenter.Panel; - Assert.Equal( - KeyboardNavigation.GetTabOnceActiveElement((InputElement)panel), - panel.Children[1]); + Assert.Equal( + KeyboardNavigation.GetTabOnceActiveElement((InputElement)panel), + panel.Children[1]); + } } [Fact] public void Removing_SelectedItem_Should_Clear_TabOnceActiveElement() { - var items = new ObservableCollection(new[] { "Foo", "Bar", "Baz " }); - - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = items, - }; + var items = new ObservableCollection(new[] { "Foo", "Bar", "Baz " }); - Prepare(target); + var target = new ListBox + { + Template = Template(), + Items = items, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + Prepare(target); - _helper.Down(target.Presenter.Panel.Children[1]); + _helper.Down(target.Presenter.Panel.Children[1]); - items.RemoveAt(1); + items.RemoveAt(1); - var panel = target.Presenter.Panel; + var panel = target.Presenter.Panel; - Assert.Null(KeyboardNavigation.GetTabOnceActiveElement((InputElement)panel)); + Assert.Null(KeyboardNavigation.GetTabOnceActiveElement((InputElement)panel)); + } } [Fact] @@ -1230,31 +1237,37 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Should_Select_Correct_Item_When_Duplicate_Items_Are_Present() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, - }; - - Prepare(target); - _helper.Down((Interactive)target.Presenter.Panel.Children[3]); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + Prepare(target); + _helper.Down((Interactive)target.Presenter.Panel.Children[3]); - Assert.Equal(3, target.SelectedIndex); + Assert.Equal(3, target.SelectedIndex); + } } [Fact] public void Should_Apply_Selected_Pseudoclass_To_Correct_Item_When_Duplicate_Items_Are_Present() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, - }; - - Prepare(target); - _helper.Down((Interactive)target.Presenter.Panel.Children[3]); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + Prepare(target); + _helper.Down((Interactive)target.Presenter.Panel.Children[3]); - Assert.Equal(new[] { ":pressed", ":selected" }, target.Presenter.Panel.Children[3].Classes); + Assert.Equal(new[] { ":pressed", ":selected" }, target.Presenter.Panel.Children[3].Classes); + } } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 6b26d76371..5d2f4e2a64 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -10,8 +10,10 @@ using Avalonia.Controls.Selection; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.UnitTests; +using Moq; using Xunit; namespace Avalonia.Controls.UnitTests.Primitives @@ -701,261 +703,290 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Shift_Selecting_From_No_Selection_Selects_From_Start() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Shift); - - var panel = target.Presenter.Panel; - - Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems); - Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target)); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Shift); + + var panel = target.Presenter.Panel; + + Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target)); + } } [Fact] public void Ctrl_Selecting_Raises_SelectionChanged_Events() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz", "Qux" }, - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Qux" }, + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); - SelectionChangedEventArgs receivedArgs = null; + SelectionChangedEventArgs receivedArgs = null; - target.SelectionChanged += (_, args) => receivedArgs = args; + target.SelectionChanged += (_, args) => receivedArgs = args; - void VerifyAdded(string selection) - { - Assert.NotNull(receivedArgs); - Assert.Equal(new[] { selection }, receivedArgs.AddedItems); - Assert.Empty(receivedArgs.RemovedItems); - } + void VerifyAdded(string selection) + { + Assert.NotNull(receivedArgs); + Assert.Equal(new[] { selection }, receivedArgs.AddedItems); + Assert.Empty(receivedArgs.RemovedItems); + } - void VerifyRemoved(string selection) - { - Assert.NotNull(receivedArgs); - Assert.Equal(new[] { selection }, receivedArgs.RemovedItems); - Assert.Empty(receivedArgs.AddedItems); - } + void VerifyRemoved(string selection) + { + Assert.NotNull(receivedArgs); + Assert.Equal(new[] { selection }, receivedArgs.RemovedItems); + Assert.Empty(receivedArgs.AddedItems); + } - _helper.Click((Interactive)target.Presenter.Panel.Children[1]); + _helper.Click((Interactive)target.Presenter.Panel.Children[1]); - VerifyAdded("Bar"); + VerifyAdded("Bar"); - receivedArgs = null; - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); + receivedArgs = null; + _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); - VerifyAdded("Baz"); + VerifyAdded("Baz"); - receivedArgs = null; - _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control); + receivedArgs = null; + _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control); - VerifyAdded("Qux"); + VerifyAdded("Qux"); - receivedArgs = null; - _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control); + receivedArgs = null; + _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control); - VerifyRemoved("Bar"); + VerifyRemoved("Bar"); + } } [Fact] public void Ctrl_Selecting_SelectedItem_With_Multiple_Selection_Active_Sets_SelectedItem_To_Next_Selection() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz", "Qux" }, - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - _helper.Click((Interactive)target.Presenter.Panel.Children[1]); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); - _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control); - - Assert.Equal(1, target.SelectedIndex); - Assert.Equal("Bar", target.SelectedItem); - Assert.Equal(new[] { "Bar", "Baz", "Qux" }, target.SelectedItems); - - _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control); - - Assert.Equal(2, target.SelectedIndex); - Assert.Equal("Baz", target.SelectedItem); - Assert.Equal(new[] { "Baz", "Qux" }, target.SelectedItems); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Qux" }, + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Click((Interactive)target.Presenter.Panel.Children[1]); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control); + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); + Assert.Equal(new[] { "Bar", "Baz", "Qux" }, target.SelectedItems); + + _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control); + + Assert.Equal(2, target.SelectedIndex); + Assert.Equal("Baz", target.SelectedItem); + Assert.Equal(new[] { "Baz", "Qux" }, target.SelectedItems); + } } [Fact] public void Ctrl_Selecting_Non_SelectedItem_With_Multiple_Selection_Active_Leaves_SelectedItem_The_Same() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, - }; + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - _helper.Click((Interactive)target.Presenter.Panel.Children[1]); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Click((Interactive)target.Presenter.Panel.Children[1]); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); - Assert.Equal(1, target.SelectedIndex); - Assert.Equal("Bar", target.SelectedItem); + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); - Assert.Equal(1, target.SelectedIndex); - Assert.Equal("Bar", target.SelectedItem); + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); + } } [Fact] public void Should_Ctrl_Select_Correct_Item_When_Duplicate_Items_Are_Present() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - _helper.Click((Interactive)target.Presenter.Panel.Children[3]); - _helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: KeyModifiers.Control); - - var panel = target.Presenter.Panel; - - Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); - Assert.Equal(new[] { 3, 4 }, SelectedContainers(target)); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Click((Interactive)target.Presenter.Panel.Children[3]); + _helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: KeyModifiers.Control); + + var panel = target.Presenter.Panel; + + Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); + Assert.Equal(new[] { 3, 4 }, SelectedContainers(target)); + } } [Fact] public void Should_Shift_Select_Correct_Item_When_Duplicates_Are_Present() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - _helper.Click((Interactive)target.Presenter.Panel.Children[3]); - _helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: KeyModifiers.Shift); - - var panel = target.Presenter.Panel; - - Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems); - Assert.Equal(new[] { 3, 4, 5 }, SelectedContainers(target)); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Click((Interactive)target.Presenter.Panel.Children[3]); + _helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: KeyModifiers.Shift); + + var panel = target.Presenter.Panel; + + Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { 3, 4, 5 }, SelectedContainers(target)); + } } [Fact] public void Can_Shift_Select_All_Items_When_Duplicates_Are_Present() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - _helper.Click((Interactive)target.Presenter.Panel.Children[0]); - _helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: KeyModifiers.Shift); - - var panel = target.Presenter.Panel; - - Assert.Equal(new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, target.SelectedItems); - Assert.Equal(new[] { 0, 1, 2, 3, 4, 5 }, SelectedContainers(target)); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + _helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: KeyModifiers.Shift); + + var panel = target.Presenter.Panel; + + Assert.Equal(new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { 0, 1, 2, 3, 4, 5 }, SelectedContainers(target)); + } } [Fact] public void Shift_Selecting_Raises_SelectionChanged_Events() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz", "Qux" }, - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Qux" }, + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); - SelectionChangedEventArgs receivedArgs = null; + SelectionChangedEventArgs receivedArgs = null; - target.SelectionChanged += (_, args) => receivedArgs = args; + target.SelectionChanged += (_, args) => receivedArgs = args; - void VerifyAdded(params string[] selection) - { - Assert.NotNull(receivedArgs); - Assert.Equal(selection, receivedArgs.AddedItems); - Assert.Empty(receivedArgs.RemovedItems); - } + void VerifyAdded(params string[] selection) + { + Assert.NotNull(receivedArgs); + Assert.Equal(selection, receivedArgs.AddedItems); + Assert.Empty(receivedArgs.RemovedItems); + } - void VerifyRemoved(string selection) - { - Assert.NotNull(receivedArgs); - Assert.Equal(new[] { selection }, receivedArgs.RemovedItems); - Assert.Empty(receivedArgs.AddedItems); - } + void VerifyRemoved(string selection) + { + Assert.NotNull(receivedArgs); + Assert.Equal(new[] { selection }, receivedArgs.RemovedItems); + Assert.Empty(receivedArgs.AddedItems); + } - _helper.Click((Interactive)target.Presenter.Panel.Children[1]); + _helper.Click((Interactive)target.Presenter.Panel.Children[1]); - VerifyAdded("Bar"); + VerifyAdded("Bar"); - receivedArgs = null; - _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Shift); + receivedArgs = null; + _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Shift); - VerifyAdded("Baz" ,"Qux"); + VerifyAdded("Baz", "Qux"); - receivedArgs = null; - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Shift); + receivedArgs = null; + _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Shift); - VerifyRemoved("Qux"); + VerifyRemoved("Qux"); + } } [Fact] public void Duplicate_Items_Are_Added_To_SelectedItems_In_Order() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, - }; + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); - Assert.Equal(new[] { "Foo" }, target.SelectedItems); + Assert.Equal(new[] { "Foo" }, target.SelectedItems); - _helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: KeyModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: KeyModifiers.Control); - Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); + Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); - _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control); - Assert.Equal(new[] { "Foo", "Bar", "Foo" }, target.SelectedItems); + Assert.Equal(new[] { "Foo", "Bar", "Foo" }, target.SelectedItems); - _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control); - Assert.Equal(new[] { "Foo", "Bar", "Foo", "Bar" }, target.SelectedItems); + Assert.Equal(new[] { "Foo", "Bar", "Foo", "Bar" }, target.SelectedItems); + } } [Fact] @@ -1158,70 +1189,79 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Left_Click_On_SelectedItem_Should_Clear_Existing_Selection() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz" }, - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - target.SelectAll(); - - Assert.Equal(3, target.SelectedItems.Count); - - _helper.Click((Interactive)target.Presenter.Panel.Children[0]); - - Assert.Equal(1, target.SelectedItems.Count); - Assert.Equal(new[] { "Foo", }, target.SelectedItems); - Assert.Equal(new[] { 0 }, SelectedContainers(target)); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.SelectAll(); + + Assert.Equal(3, target.SelectedItems.Count); + + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + + Assert.Equal(1, target.SelectedItems.Count); + Assert.Equal(new[] { "Foo", }, target.SelectedItems); + Assert.Equal(new[] { 0 }, SelectedContainers(target)); + } } [Fact] public void Right_Click_On_SelectedItem_Should_Not_Clear_Existing_Selection() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz" }, - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - target.SelectAll(); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.SelectAll(); - Assert.Equal(3, target.SelectedItems.Count); + Assert.Equal(3, target.SelectedItems.Count); - _helper.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Right); + _helper.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Right); - Assert.Equal(3, target.SelectedItems.Count); + Assert.Equal(3, target.SelectedItems.Count); + } } [Fact] public void Right_Click_On_UnselectedItem_Should_Clear_Existing_Selection() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz" }, - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - _helper.Click((Interactive)target.Presenter.Panel.Children[0]); - _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Shift); - - Assert.Equal(2, target.SelectedItems.Count); - - _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right); - - Assert.Equal(1, target.SelectedItems.Count); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Shift); + + Assert.Equal(2, target.SelectedItems.Count); + + _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right); + + Assert.Equal(1, target.SelectedItems.Count); + } } [Fact] @@ -1253,41 +1293,47 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Shift_Right_Click_Should_Not_Select_Multiple() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz" }, - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - _helper.Click((Interactive)target.Presenter.Panel.Children[0]); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: KeyModifiers.Shift); - - Assert.Equal(1, target.SelectedItems.Count); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: KeyModifiers.Shift); + + Assert.Equal(1, target.SelectedItems.Count); + } } [Fact] public void Ctrl_Right_Click_Should_Not_Select_Multiple() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz" }, - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - _helper.Click((Interactive)target.Presenter.Panel.Children[0]); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: KeyModifiers.Control); - - Assert.Equal(1, target.SelectedItems.Count); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: KeyModifiers.Control); + + Assert.Equal(1, target.SelectedItems.Count); + } } [Fact] From 705e9065a3b677359a9db1fadd7665dec96bb050 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 3 Aug 2022 02:28:05 -0400 Subject: [PATCH 06/20] Add more tests for lazy resources --- .../Xaml/ResourceDictionaryTests.cs | 84 +++++++++++++++++-- 1 file changed, 79 insertions(+), 5 deletions(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs index 939c5319e4..74b6a1e15d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs @@ -74,13 +74,33 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml var xaml = @" - Red + "; var resources = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(xaml); Assert.True(resources.ContainsDeferredKey("Red")); } } + + [Fact] + public void Item_Added_To_ResourceDictionary_Is_UnDeferred_On_Read() + { + using (StyledWindow()) + { + var xaml = @" + + +"; + var resources = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(xaml); + + Assert.True(resources.ContainsDeferredKey("Red")); + + Assert.IsType(resources["Red"]); + + Assert.False(resources.ContainsDeferredKey("Red")); + } + } [Fact] public void Item_Is_Added_To_Window_Resources_As_Deferred() @@ -91,7 +111,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml - Red + "; var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); @@ -113,7 +133,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml - Red + @@ -135,7 +155,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml "; var style = (Style)AvaloniaRuntimeXamlLoader.Load(xaml); @@ -154,7 +174,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml - Red + "; var style = (Styles)AvaloniaRuntimeXamlLoader.Load(xaml); @@ -164,6 +184,60 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [Fact] + public void Item_Can_Be_StaticReferenced_As_Deferred() + { + using (StyledWindow()) + { + var xaml = @" + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var windowResources = (ResourceDictionary)window.Resources; + var buttonResources = (ResourceDictionary)((Button)window.Content!).Resources; + + Assert.True(windowResources.ContainsDeferredKey("Red")); + Assert.True(buttonResources.ContainsDeferredKey("Red2")); + } + } + + [Fact] + public void Item_StaticReferenced_Is_UnDeferred_On_Read() + { + using (StyledWindow()) + { + var xaml = @" + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var windowResources = (ResourceDictionary)window.Resources; + var buttonResources = (ResourceDictionary)((Button)window.Content!).Resources; + + Assert.IsType(buttonResources["Red2"]); + + Assert.False(windowResources.ContainsDeferredKey("Red")); + Assert.False(buttonResources.ContainsDeferredKey("Red2")); + } + } + private IDisposable StyledWindow(params (string, string)[] assets) { var services = TestServices.StyledWindow.With( From 3fd8763e807f195cee6c4313cf6e0ecf44e74aa5 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Wed, 3 Aug 2022 13:22:00 +0300 Subject: [PATCH 07/20] Use correct ToggleModifier in TreeView multiselection on MacOS. --- src/Avalonia.Controls/TreeView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index b2a188a2ea..7359f3cade 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -529,7 +529,7 @@ namespace Avalonia.Controls e.Source, true, e.KeyModifiers.HasAllFlags(KeyModifiers.Shift), - e.KeyModifiers.HasAllFlags(KeyModifiers.Control), + e.KeyModifiers.HasAllFlags(AvaloniaLocator.Current.GetRequiredService().CommandModifiers), point.Properties.IsRightButtonPressed); } } From a30d73eb5ed9433cff2d212b21fb9eae93e7b54b Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Wed, 3 Aug 2022 14:38:45 +0300 Subject: [PATCH 08/20] Fix tests. --- .../TreeViewTests.cs | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index d784caf2db..9cf21423a3 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -18,6 +18,7 @@ using Avalonia.LogicalTree; using Avalonia.Styling; using Avalonia.UnitTests; using JetBrains.Annotations; +using Moq; using Xunit; namespace Avalonia.Controls.UnitTests @@ -885,28 +886,31 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Right_Click_On_SelectedItem_Should_Not_Clear_Existing_Selection() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (UnitTestApplication.Start()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple, - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); - target.SelectAll(); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + target.SelectAll(); - AssertChildrenSelected(target, tree[0]); - Assert.Equal(5, target.SelectedItems.Count); + AssertChildrenSelected(target, tree[0]); + Assert.Equal(5, target.SelectedItems.Count); - _mouse.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Right); + _mouse.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Right); - Assert.Equal(5, target.SelectedItems.Count); + Assert.Equal(5, target.SelectedItems.Count); + } } [Fact] From fffdcfcc2a8c49f16d9f1c363c774e56e20d685b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 3 Aug 2022 14:05:55 +0200 Subject: [PATCH 09/20] Fix fluent ButtonSpinner theme. The button was greying out its borders on pointer-over. --- .../Controls/ButtonSpinner.xaml | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml index 855dc5363e..aa55065f6d 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml @@ -41,13 +41,31 @@ M0,9 L10,0 20,9 19,10 10,2 1,10 z M0,1 L10,10 20,1 19,0 10,8 1,0 z - - + - + + + + + + + + + @@ -83,7 +101,6 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness, Converter={StaticResource ButtonSpinnerLeftThickness}}" - CornerRadius="0" VerticalAlignment="Stretch" VerticalContentAlignment="Center" Foreground="{TemplateBinding Foreground}" @@ -99,7 +116,6 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness, Converter={StaticResource ButtonSpinnerLeftThickness}}" - CornerRadius="0" VerticalAlignment="Stretch" VerticalContentAlignment="Center" Foreground="{TemplateBinding Foreground}" From ad518ac6787ab8265adcd1a4eddc70c390590ca6 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 3 Aug 2022 15:00:42 +0300 Subject: [PATCH 10/20] Add backpressure for sending batches to the render thread --- .../Rendering/Composition/CompositingRenderer.cs | 2 +- src/Avalonia.Base/Rendering/Composition/Compositor.cs | 11 ++++++++++- .../Rendering/Composition/Server/ServerCompositor.cs | 3 +-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index db773bc43c..9aa3c25425 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -71,7 +71,7 @@ public class CompositingRenderer : IRendererWithCompositor if(_queuedUpdate) return; _queuedUpdate = true; - Dispatcher.UIThread.Post(_update, DispatcherPriority.Composition); + _compositor.InvokeWhenReadyForNextCommit(_update); } /// diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 45212d0f36..f812acc016 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -33,6 +33,7 @@ namespace Avalonia.Rendering.Composition internal IEasing DefaultEasing { get; } private List? _invokeOnNextCommit; private readonly Stack> _invokeListPool = new(); + private Task? _lastBatchCompleted; /// /// Creates a new compositor on a specified render loop that would use a particular GPU @@ -86,7 +87,7 @@ namespace Avalonia.Rendering.Composition if (_invokeOnNextCommit != null) ScheduleCommitCallbacks(batch.Completed); - return batch.Completed; + return _lastBatchCompleted = batch.Completed; } async void ScheduleCommitCallbacks(Task task) @@ -139,5 +140,13 @@ namespace Avalonia.Rendering.Composition _invokeOnNextCommit ??= _invokeListPool.Count > 0 ? _invokeListPool.Pop() : new(); _invokeOnNextCommit.Add(action); } + + public void InvokeWhenReadyForNextCommit(Action action) + { + if (_lastBatchCompleted == null || _lastBatchCompleted.IsCompleted) + Dispatcher.UIThread.Post(action, DispatcherPriority.Composition); + else + _lastBatchCompleted.ContinueWith(_ => Dispatcher.UIThread.Post(action, DispatcherPriority.Composition)); + } } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index 621bc84f4a..bfc2b2d626 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -108,6 +108,7 @@ namespace Avalonia.Rendering.Composition.Server private void RenderCore() { ApplyPendingBatches(); + CompletePendingBatches(); foreach(var animation in _activeAnimations) _animationsToUpdate.Add(animation); @@ -119,8 +120,6 @@ namespace Avalonia.Rendering.Composition.Server foreach (var t in _activeTargets) t.Render(); - - CompletePendingBatches(); } public void AddCompositionTarget(ServerCompositionTarget target) From 0d472a100b6fc191d69675512028c4e770045081 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 3 Aug 2022 15:57:13 +0300 Subject: [PATCH 11/20] Use static callback for ContinueWith --- src/Avalonia.Base/Rendering/Composition/Compositor.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index f812acc016..10360f7874 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -146,7 +146,9 @@ namespace Avalonia.Rendering.Composition if (_lastBatchCompleted == null || _lastBatchCompleted.IsCompleted) Dispatcher.UIThread.Post(action, DispatcherPriority.Composition); else - _lastBatchCompleted.ContinueWith(_ => Dispatcher.UIThread.Post(action, DispatcherPriority.Composition)); + _lastBatchCompleted.ContinueWith( + static (_, state) => Dispatcher.UIThread.Post((Action)state!, DispatcherPriority.Composition), + action); } } } From 3d327bc046ab6295fac0634b9da06fec92a8d493 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Wed, 3 Aug 2022 18:50:23 +0300 Subject: [PATCH 12/20] Fix and optimize StringFormatConverter. StringFormatConverter should validate values and return AvaloniaProperty.UnsetValue in case of incorrect value or exception. --- .../Converters/StringFormatConverter.cs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/Converters/StringFormatConverter.cs b/src/Avalonia.Controls/Converters/StringFormatConverter.cs index ae920dac7e..7075d3de99 100644 --- a/src/Avalonia.Controls/Converters/StringFormatConverter.cs +++ b/src/Avalonia.Controls/Converters/StringFormatConverter.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; -using Avalonia.Data; using Avalonia.Data.Converters; namespace Avalonia.Controls.Converters; @@ -15,13 +13,25 @@ public class StringFormatConverter : IMultiValueConverter { public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) { - try - { - return string.Format((string)values[0]!, values.Skip(1).ToArray()); - } - catch (Exception e) + if (values != null && + values.Count == 5 && + values[0] is string format && + values[1] is double && + values[2] is double && + values[3] is double && + values[4] is double) + { - return new BindingNotification(e, BindingErrorType.Error); + + try + { + return string.Format(format, values[1], values[2], values[3], values[4]); + } + catch + { + return AvaloniaProperty.UnsetValue; + } } + return AvaloniaProperty.UnsetValue; } } From d4840a55bc1037970cc8cc2e2125fb0446741ebb Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 2 Aug 2022 18:31:57 +0300 Subject: [PATCH 13/20] add failing test for #8669 --- .../ListBoxTests.cs | 49 +++++++++++++++++-- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index afa153a593..f6d96edb99 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -9,7 +9,6 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Styling; -using Avalonia.Threading; using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; @@ -19,7 +18,7 @@ namespace Avalonia.Controls.UnitTests public class ListBoxTests { private MouseTestHelper _mouse = new MouseTestHelper(); - + [Fact] public void Should_Use_ItemTemplate_To_Create_Item_Content() { @@ -433,6 +432,47 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void ListBox_Should_Be_Valid_After_Remove_Of_Item_In_NonVisibleArea() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var items = new AvaloniaList(Enumerable.Range(1, 30).Select(v => v.ToString())); + + var wnd = new Window() { Width = 100, Height = 100, IsVisible = true }; + + var target = new ListBox() + { + AutoScrollToSelectedItem = true, + Height = 100, + Width = 50, + VirtualizationMode = ItemVirtualizationMode.Simple, + ItemTemplate = new FuncDataTemplate((c, _) => new Border() { Height = 10 }), + Items = items, + }; + wnd.Content = target; + + var lm = wnd.LayoutManager; + + lm.ExecuteInitialLayoutPass(); + + //select last / scroll to last item + target.SelectedItem = items.Last(); + + lm.ExecuteLayoutPass(); + + //remove the first item (in non realized area of the listbox) + items.Remove("1"); + lm.ExecuteLayoutPass(); + + Assert.Equal("30", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 1).DataContext); + Assert.Equal("29", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 2).DataContext); + Assert.Equal("28", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 3).DataContext); + Assert.Equal("27", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 4).DataContext); + Assert.Equal("26", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 5).DataContext); + } + } + [Fact] public void Clicking_Item_Should_Raise_BringIntoView_For_Correct_Control() { @@ -656,7 +696,6 @@ namespace Avalonia.Controls.UnitTests public string Value { get; } } - [Fact] public void SelectedItem_Validation() { @@ -670,11 +709,11 @@ namespace Avalonia.Controls.UnitTests }; Prepare(target); - + var exception = new System.InvalidCastException("failed validation"); var textObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError)); target.Bind(ComboBox.SelectedItemProperty, textObservable); - + Assert.True(DataValidationErrors.GetHasErrors(target)); Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); } From d68efa1cd4f89142eb7cf159425ea6939d978e02 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 4 Aug 2022 15:07:23 +0300 Subject: [PATCH 14/20] fix for #8669 --- src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 39a512a773..4db2ec6d50 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -187,7 +187,7 @@ namespace Avalonia.Controls.Presenters break; case NotifyCollectionChangedAction.Remove: - if ((e.OldStartingIndex >= FirstIndex && e.OldStartingIndex < NextIndex) || + if (e.OldStartingIndex < NextIndex || panel.Children.Count > ItemCount) { RecycleContainersOnRemove(); From 6520722e5488350433e525232877e139ad57cf53 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 3 Aug 2022 03:20:37 -0400 Subject: [PATCH 15/20] Attempt to not deferr value types --- ...valoniaXamlIlDeferredResourceTransformer.cs | 10 ++++++++++ .../Xaml/ResourceDictionaryTests.cs | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs index 099878df08..662263e513 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs @@ -15,6 +15,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers if (!(node is XamlPropertyAssignmentNode pa) || pa.Values.Count != 2) return node; + if (!ShouldBeDeferred(pa.Values[1])) + return node; + var types = context.GetAvaloniaTypes(); if (pa.Property.DeclaringType == types.ResourceDictionary && pa.Property.Name == "Content") @@ -37,6 +40,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers return node; } + private static bool ShouldBeDeferred(IXamlAstValueNode node) + { + // XAML compiler is currently strict about value types, allowing them to be created only through converters. + // At the moment it should be safe to not defer structs. + return !node.Type.GetClrType().IsValueType; + } + class AdderSetter : IXamlPropertySetter, IXamlEmitablePropertySetter { private readonly IXamlMethod _getter; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs index 74b6a1e15d..19fb88b391 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs @@ -3,6 +3,7 @@ using Avalonia.Controls; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Styling; using Avalonia.UnitTests; using Xunit; @@ -237,6 +238,23 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.False(buttonResources.ContainsDeferredKey("Red2")); } } + + [Fact] + public void Value_Type_With_Parse_Should_Not_Be_Deferred() + { + using (StyledWindow()) + { + var xaml = @" + + Red +"; + var resources = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(xaml); + + Assert.False(resources.ContainsDeferredKey("Red")); + Assert.IsType(resources["Red"]); + } + } private IDisposable StyledWindow(params (string, string)[] assets) { From 41d31d8857dec976aa293d8070ea69296f84c2d1 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 3 Aug 2022 03:53:01 -0400 Subject: [PATCH 16/20] Add thickness test as well --- .../Xaml/ResourceDictionaryTests.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs index 19fb88b391..5066341bbb 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs @@ -240,7 +240,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } [Fact] - public void Value_Type_With_Parse_Should_Not_Be_Deferred() + public void Value_Type_With_Parse_Converter_Should_Not_Be_Deferred() { using (StyledWindow()) { @@ -255,6 +255,23 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.IsType(resources["Red"]); } } + + [Fact] + public void Value_Type_With_Ctor_Converter_Should_Not_Be_Deferred() + { + using (StyledWindow()) + { + var xaml = @" + + 1 1 1 1 +"; + var resources = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(xaml); + + Assert.False(resources.ContainsDeferredKey("Margin")); + Assert.IsType(resources["Margin"]); + } + } private IDisposable StyledWindow(params (string, string)[] assets) { From 0360af6780f476f195142fa2772fc9f008e9da47 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 4 Aug 2022 18:11:07 +0200 Subject: [PATCH 17/20] Added failing test for #8672. --- .../Data/TemplateBindingTests.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs index d9ea3e374c..979dbec674 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using Avalonia.Controls; @@ -217,6 +218,37 @@ namespace Avalonia.Markup.UnitTests.Data } } + [Fact] + public void Should_Not_Pass_UnsetValue_To_MultiBinding_During_ApplyTemplate() + { + var converter = new MultiConverter(); + var source = new Button + { + Content = "foo", + Template = new FuncControlTemplate