From 220a9874679be2dd50adee0f6cda6f189a526023 Mon Sep 17 00:00:00 2001 From: donandren Date: Sat, 14 Jan 2017 16:24:58 +0200 Subject: [PATCH 01/61] issue #855 unit test --- .../ListBoxTests_Single.cs | 71 ++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs index c7992fe80f..d95acbdb7f 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs @@ -1,11 +1,15 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Input; using Avalonia.LogicalTree; +using Avalonia.Markup.Xaml.Data; using Avalonia.Styling; using Avalonia.VisualTree; using Xunit; @@ -199,6 +203,71 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(1, target.SelectedIndex); } + [Fact] + public void SelectedItem_Should_Not_Cause_StackOverflow() + { + var viewModel = new TestStackOverflowViewModel() + { + Items = new List { "foo", "bar", "baz" } + }; + + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + DataContext = viewModel, + Items = viewModel.Items + }; + + target.Bind(ListBox.SelectedItemProperty, + new Binding("SelectedItem") { Mode = BindingMode.TwoWay }); + + Assert.Equal(0, viewModel.SetterInvokedCount); + + //here in real life stack overflow exception is thrown issue #855 + target.SelectedItem = viewModel.Items[2]; + + Assert.Equal(viewModel.Items[1], target.SelectedItem); + Assert.Equal(1, viewModel.SetterInvokedCount); + } + + private class TestStackOverflowViewModel : INotifyPropertyChanged + { + public List Items { get; set; } + + public int SetterInvokedCount { get; private set; } + + public const int MaxInvokedCount = 1000; + + private string _selectedItem; + + public event PropertyChangedEventHandler PropertyChanged; + + public string SelectedItem + { + get { return _selectedItem; } + set + { + if (_selectedItem != value) + { + SetterInvokedCount++; + + int index = Items.IndexOf(value); + + if (MaxInvokedCount > SetterInvokedCount && index > 0) + { + _selectedItem = Items[index - 1]; + } + else + { + _selectedItem = value; + } + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedItem))); + } + } + } + } + private Control CreateListBoxTemplate(ITemplatedControl parent) { return new ScrollViewer @@ -237,4 +306,4 @@ namespace Avalonia.Controls.UnitTests target.Presenter.ApplyTemplate(); } } -} +} \ No newline at end of file From 3729b9797fbae73bd1b0f30204d3d01d8c8493b4 Mon Sep 17 00:00:00 2001 From: donandren Date: Sun, 15 Jan 2017 16:06:43 +0200 Subject: [PATCH 02/61] another failing test for issue #855 and #824 --- .../Primitives/RangeBaseTests.cs | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs index d3ed077cbf..9bccb1986d 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs @@ -2,7 +2,11 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.ComponentModel; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Markup.Xaml.Data; using Xunit; namespace Avalonia.Controls.UnitTests.Primitives @@ -87,8 +91,93 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Throws(() => target.Value = double.NegativeInfinity); } + [Fact] + public void SetValue_Should_Not_Cause_StackOverflow() + { + var viewModel = new TestStackOverflowViewModel() + { + Value = 50 + }; + + Track track = null; + + var target = new TestRange() + { + Template = new FuncControlTemplate(c => + { + return track = new Track() + { + Width = 100, + Orientation = Orientation.Horizontal, + [~~Track.MinimumProperty] = c[~~RangeBase.MinimumProperty], + [~~Track.MaximumProperty] = c[~~RangeBase.MaximumProperty], + [~~Track.ValueProperty] = c[~~RangeBase.ValueProperty], + Name = "PART_Track", + Thumb = new Thumb() + }; + }), + Minimum = 0, + Maximum = 100, + DataContext = viewModel + }; + + target.Bind(TestRange.ValueProperty, new Binding("Value") { Mode = BindingMode.TwoWay }); + + target.ApplyTemplate(); + track.Measure(new Size(100, 0)); + track.Arrange(new Rect(0, 0, 100, 0)); + + Assert.Equal(1, viewModel.SetterInvokedCount); + + //here in real life stack overflow exception is thrown issue #855 and #824 + target.Value = 51.001; + + Assert.Equal(2, viewModel.SetterInvokedCount); + + double expected = 51; + + Assert.Equal(expected, viewModel.Value); + Assert.Equal(expected, target.Value); + Assert.Equal(expected, track.Value); + } + private class TestRange : RangeBase { } + + private class TestStackOverflowViewModel : INotifyPropertyChanged + { + public int SetterInvokedCount { get; private set; } + + public const int MaxInvokedCount = 1000; + + private double _value; + + public event PropertyChangedEventHandler PropertyChanged; + + public double Value + { + get { return _value; } + set + { + if (_value != value) + { + SetterInvokedCount++; + if (SetterInvokedCount < MaxInvokedCount) + { + _value = (int)value; + if (_value > 75) _value = 75; + if (_value < 25) _value = 25; + } + else + { + _value = value; + } + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value))); + } + } + } + } } } \ No newline at end of file From ef81c5596051ac123a46c9165b455c48d3bcb06d Mon Sep 17 00:00:00 2001 From: donandren Date: Fri, 17 Feb 2017 00:32:38 +0200 Subject: [PATCH 03/61] another simple unit tests for issue #855 for direct and styled properties --- .../Avalonia.Base.UnitTests.csproj | 4 + .../AvaloniaObjectTests_Binding.cs | 103 +++++++++++++++-- .../AvaloniaObjectTests_Direct.cs | 106 ++++++++++++++++-- 3 files changed, 191 insertions(+), 22 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj index 07ed7f14ca..c712a8bcf9 100644 --- a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj +++ b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj @@ -124,6 +124,10 @@ {B09B78D8-9B26-48B0-9149-D64A2F120F3F} Avalonia.Base + + {3E53A01A-B331-47F3-B828-4A5717E77A24} + Avalonia.Markup.Xaml + {88060192-33d5-4932-b0f9-8bd2763e857d} Avalonia.UnitTests diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 5e286305d2..7186207b92 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -2,22 +2,21 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections; -using System.Collections.Generic; +using System.ComponentModel; +using System.Reactive.Concurrency; using System.Reactive.Linq; using System.Reactive.Subjects; -using Microsoft.Reactive.Testing; +using System.Threading; +using System.Threading.Tasks; using Avalonia.Data; using Avalonia.Logging; -using Avalonia.UnitTests; -using Xunit; -using System.Threading.Tasks; +using Avalonia.Markup.Xaml.Data; using Avalonia.Platform; -using System.Threading; -using Moq; -using System.Reactive.Disposables; -using System.Reactive.Concurrency; using Avalonia.Threading; +using Avalonia.UnitTests; +using Microsoft.Reactive.Testing; +using Moq; +using Xunit; namespace Avalonia.Base.UnitTests { @@ -363,7 +362,7 @@ namespace Avalonia.Base.UnitTests Assert.True(called); } } - + [Fact] public async void Bind_With_Scheduler_Executes_On_Scheduler() { @@ -384,7 +383,43 @@ namespace Avalonia.Base.UnitTests await Task.Run(() => source.OnNext(6.7)); } + } + + [Fact] + public void SetValue_Should_Not_Cause_StackOverflow_And_Have_Correct_Values() + { + var viewModel = new TestStackOverflowViewModel() + { + Value = 50 + }; + + var target = new Class1(); + + //note: if the initialization of the child binding is here target/child binding work fine!!! + //var child = new Class1() + //{ + // [~~Class1.DoubleValueProperty] = target[~~Class1.DoubleValueProperty] + //}; + target.Bind(Class1.DoubleValueProperty, new Binding("Value") { Mode = BindingMode.TwoWay, Source = viewModel }); + + var child = new Class1() + { + [~~Class1.DoubleValueProperty] = target[~~Class1.DoubleValueProperty] + }; + + Assert.Equal(1, viewModel.SetterInvokedCount); + + //here in real life stack overflow exception is thrown issue #855 and #824 + target.DoubleValue = 51.001; + + Assert.Equal(2, viewModel.SetterInvokedCount); + + double expected = 51; + + Assert.Equal(expected, viewModel.Value); + Assert.Equal(expected, target.DoubleValue); + Assert.Equal(expected, child.DoubleValue); } /// @@ -405,6 +440,15 @@ namespace Avalonia.Base.UnitTests public static readonly StyledProperty QuxProperty = AvaloniaProperty.Register("Qux", 5.6); + + public static readonly StyledProperty DoubleValueProperty = + AvaloniaProperty.Register(nameof(DoubleValue)); + + public double DoubleValue + { + get { return GetValue(DoubleValueProperty); } + set { SetValue(DoubleValueProperty, value); } + } } private class Class2 : Class1 @@ -431,5 +475,40 @@ namespace Avalonia.Base.UnitTests return new InstancedBinding(_source, BindingMode.OneTime); } } + + private class TestStackOverflowViewModel : INotifyPropertyChanged + { + public int SetterInvokedCount { get; private set; } + + public const int MaxInvokedCount = 1000; + + private double _value; + + public event PropertyChangedEventHandler PropertyChanged; + + public double Value + { + get { return _value; } + set + { + if (_value != value) + { + SetterInvokedCount++; + if (SetterInvokedCount < MaxInvokedCount) + { + _value = (int)value; + if (_value > 75) _value = 75; + if (_value < 25) _value = 25; + } + else + { + _value = value; + } + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value))); + } + } + } + } } -} +} \ No newline at end of file diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs index ecb555252d..b6e5396020 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs @@ -3,10 +3,11 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Reactive.Subjects; -using Avalonia; using Avalonia.Data; using Avalonia.Logging; +using Avalonia.Markup.Xaml.Data; using Avalonia.UnitTests; using Xunit; @@ -208,7 +209,7 @@ namespace Avalonia.Base.UnitTests { var target = new Class1(); - Assert.Throws(() => + Assert.Throws(() => target.SetValue(Class1.BarProperty, "newvalue")); } @@ -217,7 +218,7 @@ namespace Avalonia.Base.UnitTests { var target = new Class1(); - Assert.Throws(() => + Assert.Throws(() => target.SetValue((AvaloniaProperty)Class1.BarProperty, "newvalue")); } @@ -227,7 +228,7 @@ namespace Avalonia.Base.UnitTests var target = new Class1(); var source = new Subject(); - Assert.Throws(() => + Assert.Throws(() => target.Bind(Class1.BarProperty, source)); } @@ -439,12 +440,49 @@ namespace Avalonia.Base.UnitTests Assert.Equal(BindingMode.OneWayToSource, bar.GetMetadata().DefaultBindingMode); } + [Fact] + public void SetValue_Should_Not_Cause_StackOverflow_And_Have_Correct_Values() + { + var viewModel = new TestStackOverflowViewModel() + { + Value = 50 + }; + + var target = new Class1(); + + //note: if the initialization of the child binding is here there is no stackoverflow!!! + //var child = new Class1() + //{ + // [~~Class1.DoubleValueProperty] = target[~~Class1.DoubleValueProperty] + //}; + + target.Bind(Class1.DoubleValueProperty, new Binding("Value") { Mode = BindingMode.TwoWay, Source = viewModel }); + + var child = new Class1() + { + [~~Class1.DoubleValueProperty] = target[~~Class1.DoubleValueProperty] + }; + + Assert.Equal(1, viewModel.SetterInvokedCount); + + //here in real life stack overflow exception is thrown issue #855 and #824 + target.DoubleValue = 51.001; + + Assert.Equal(2, viewModel.SetterInvokedCount); + + double expected = 51; + + Assert.Equal(expected, viewModel.Value); + Assert.Equal(expected, target.DoubleValue); + Assert.Equal(expected, child.DoubleValue); + } + private class Class1 : AvaloniaObject { public static readonly DirectProperty FooProperty = AvaloniaProperty.RegisterDirect( - "Foo", - o => o.Foo, + "Foo", + o => o.Foo, (o, v) => o.Foo = v, unsetValue: "unset"); @@ -453,14 +491,21 @@ namespace Avalonia.Base.UnitTests public static readonly DirectProperty BazProperty = AvaloniaProperty.RegisterDirect( - "Bar", - o => o.Baz, - (o,v) => o.Baz = v, + "Bar", + o => o.Baz, + (o, v) => o.Baz = v, unsetValue: -1); + public static readonly DirectProperty DoubleValueProperty = + AvaloniaProperty.RegisterDirect( + nameof(DoubleValue), + o => o.DoubleValue, + (o, v) => o.DoubleValue = v); + private string _foo = "initial"; private readonly string _bar = "bar"; private int _baz = 5; + private double _doubleValue; public string Foo { @@ -478,6 +523,12 @@ namespace Avalonia.Base.UnitTests get { return _baz; } set { SetAndRaise(BazProperty, ref _baz, value); } } + + public double DoubleValue + { + get { return _doubleValue; } + set { SetAndRaise(DoubleValueProperty, ref _doubleValue, value); } + } } private class Class2 : AvaloniaObject @@ -497,5 +548,40 @@ namespace Avalonia.Base.UnitTests set { SetAndRaise(FooProperty, ref _foo, value); } } } + + private class TestStackOverflowViewModel : INotifyPropertyChanged + { + public int SetterInvokedCount { get; private set; } + + public const int MaxInvokedCount = 1000; + + private double _value; + + public event PropertyChangedEventHandler PropertyChanged; + + public double Value + { + get { return _value; } + set + { + if (_value != value) + { + SetterInvokedCount++; + if (SetterInvokedCount < MaxInvokedCount) + { + _value = (int)value; + if (_value > 75) _value = 75; + if (_value < 25) _value = 25; + } + else + { + _value = value; + } + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value))); + } + } + } + } } -} +} \ No newline at end of file From 8f3ce463f0c1e97642b43d8727525a6f95fcab40 Mon Sep 17 00:00:00 2001 From: donandren Date: Sun, 26 Feb 2017 21:27:44 +0200 Subject: [PATCH 04/61] Slider (RangeBase) test for #824 with Binding which behind scenes is using weakobservable --- .../Primitives/RangeBaseTests.cs | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs index 9bccb1986d..0e67581760 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs @@ -7,6 +7,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Markup.Xaml.Data; +using Avalonia.Styling; using Xunit; namespace Avalonia.Controls.UnitTests.Primitives @@ -91,8 +92,10 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Throws(() => target.Value = double.NegativeInfinity); } - [Fact] - public void SetValue_Should_Not_Cause_StackOverflow() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SetValue_Should_Not_Cause_StackOverflow(bool useXamlBinding) { var viewModel = new TestStackOverflowViewModel() { @@ -105,16 +108,32 @@ namespace Avalonia.Controls.UnitTests.Primitives { Template = new FuncControlTemplate(c => { - return track = new Track() + track = new Track() { Width = 100, Orientation = Orientation.Horizontal, [~~Track.MinimumProperty] = c[~~RangeBase.MinimumProperty], [~~Track.MaximumProperty] = c[~~RangeBase.MaximumProperty], - [~~Track.ValueProperty] = c[~~RangeBase.ValueProperty], + Name = "PART_Track", Thumb = new Thumb() }; + + if (useXamlBinding) + { + track.Bind(Track.ValueProperty, new Binding("Value") + { + Mode = BindingMode.TwoWay, + Source = c, + Priority = BindingPriority.Style + }); + } + else + { + track[~~Track.ValueProperty] = c[~~RangeBase.ValueProperty]; + } + + return track; }), Minimum = 0, Maximum = 100, From ce387271cc3f77a6ba622cf66402ef6f9c83de9c Mon Sep 17 00:00:00 2001 From: donandren Date: Sun, 26 Feb 2017 23:54:24 +0200 Subject: [PATCH 05/61] add test parameters to styled/direct binding tests for #855 and #824 --- .../AvaloniaObjectTests_Binding.cs | 26 +++++++++++++---- .../AvaloniaObjectTests_Direct.cs | 29 +++++++++++++++---- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 7186207b92..95aaa1fa32 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -385,8 +385,10 @@ namespace Avalonia.Base.UnitTests } } - [Fact] - public void SetValue_Should_Not_Cause_StackOverflow_And_Have_Correct_Values() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SetValue_Should_Not_Cause_StackOverflow_And_Have_Correct_Values(bool useXamlBinding) { var viewModel = new TestStackOverflowViewModel() { @@ -401,12 +403,24 @@ namespace Avalonia.Base.UnitTests // [~~Class1.DoubleValueProperty] = target[~~Class1.DoubleValueProperty] //}; - target.Bind(Class1.DoubleValueProperty, new Binding("Value") { Mode = BindingMode.TwoWay, Source = viewModel }); + target.Bind(Class1.DoubleValueProperty, + new Binding("Value") { Mode = BindingMode.TwoWay, Source = viewModel }); + + var child = new Class1(); - var child = new Class1() + if (useXamlBinding) { - [~~Class1.DoubleValueProperty] = target[~~Class1.DoubleValueProperty] - }; + child.Bind(Class1.DoubleValueProperty, + new Binding("DoubleValue") + { + Mode = BindingMode.TwoWay, + Source = target + }); + } + else + { + child[!!Class1.DoubleValueProperty] = target[!!Class1.DoubleValueProperty]; + } Assert.Equal(1, viewModel.SetterInvokedCount); diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs index b6e5396020..17af8164b6 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs @@ -440,8 +440,10 @@ namespace Avalonia.Base.UnitTests Assert.Equal(BindingMode.OneWayToSource, bar.GetMetadata().DefaultBindingMode); } - [Fact] - public void SetValue_Should_Not_Cause_StackOverflow_And_Have_Correct_Values() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SetValue_Should_Not_Cause_StackOverflow_And_Have_Correct_Values(bool useXamlBinding) { var viewModel = new TestStackOverflowViewModel() { @@ -456,12 +458,27 @@ namespace Avalonia.Base.UnitTests // [~~Class1.DoubleValueProperty] = target[~~Class1.DoubleValueProperty] //}; - target.Bind(Class1.DoubleValueProperty, new Binding("Value") { Mode = BindingMode.TwoWay, Source = viewModel }); + target.Bind(Class1.DoubleValueProperty, new Binding("Value") + { + Mode = BindingMode.TwoWay, + Source = viewModel + }); + + var child = new Class1(); - var child = new Class1() + if (useXamlBinding) { - [~~Class1.DoubleValueProperty] = target[~~Class1.DoubleValueProperty] - }; + child.Bind(Class1.DoubleValueProperty, + new Binding("DoubleValue") + { + Mode = BindingMode.TwoWay, + Source = target + }); + } + else + { + child[!!Class1.DoubleValueProperty] = target[!!Class1.DoubleValueProperty]; + } Assert.Equal(1, viewModel.SetterInvokedCount); From 4f13d504967aa9be3ccd2123189dece3591d32ad Mon Sep 17 00:00:00 2001 From: Jurjen Biewenga Date: Thu, 28 Sep 2017 15:53:05 +0200 Subject: [PATCH 06/61] Added initial changes to allow the user to change the selected item while the dropdown is closed but selected and allows the user to open the dropdown by pressing down --- src/Avalonia.Controls/DropDown.cs | 24 ++++++++++++++++++++++++ src/Avalonia.Controls/ItemsControl.cs | 11 +++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/Avalonia.Controls/DropDown.cs b/src/Avalonia.Controls/DropDown.cs index 5349fb1ca7..63fd6726b4 100644 --- a/src/Avalonia.Controls/DropDown.cs +++ b/src/Avalonia.Controls/DropDown.cs @@ -114,6 +114,30 @@ namespace Avalonia.Controls IsDropDownOpen = false; e.Handled = true; } + + if (!IsDropDownOpen) + { + if (e.Key == Key.Right) + { + if (++SelectedIndex >= ItemCount) + SelectedIndex = 0; + + e.Handled = true; + } + else if (e.Key == Key.Left) + { + if (--SelectedIndex < 0) + SelectedIndex = ItemCount - 1; + e.Handled = true; + } + else if (e.Key == Key.Down) + { + IsDropDownOpen = true; + if (SelectedIndex == -1) + SelectedIndex = 0; + e.Handled = true; + } + } } } diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index aa209e0462..4366de1cd6 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -11,6 +11,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; +using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Metadata; @@ -106,6 +107,12 @@ namespace Avalonia.Controls set { SetAndRaise(ItemsProperty, ref _items, value); } } + public int ItemCount + { + get; + private set; + } + /// /// Gets or sets the panel used to display the items. /// @@ -352,6 +359,10 @@ namespace Avalonia.Controls RemoveControlItemsFromLogicalChildren(e.OldItems); break; } + + int? count = (Items as IList)?.Count; + if (count != null) + ItemCount = (int)count; var collection = sender as ICollection; PseudoClasses.Set(":empty", collection == null || collection.Count == 0); From 12e32dc08e9ac77c51860df9f6fe0f16d2ea5a6e Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Mon, 9 Oct 2017 15:31:38 -0500 Subject: [PATCH 07/61] Implement RelativeSource binding syntax sugar except for parsing AncestorType. --- .../Avalonia.Markup.Xaml/Data/Binding.cs | 96 +++++++++++++++++-- 1 file changed, 87 insertions(+), 9 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs index 3f12593197..9d911999c6 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs @@ -96,20 +96,23 @@ namespace Avalonia.Markup.Xaml.Data ValidateState(pathInfo); enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue; + var elementName = pathInfo.ElementName; + var relativeSource = RelativeSource ?? pathInfo.RelativeSource; + ExpressionObserver observer; - if (pathInfo.ElementName != null || ElementName != null) + if (elementName != null) { observer = CreateElementObserver( (target as IControl) ?? (anchor as IControl), - pathInfo.ElementName ?? ElementName, + elementName, pathInfo.Path); } else if (Source != null) { observer = CreateSourceObserver(Source, pathInfo.Path, enableDataValidation); } - else if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext) + else if (relativeSource == null || relativeSource.Mode == RelativeSourceMode.DataContext) { observer = CreateDataContexObserver( target, @@ -118,23 +121,24 @@ namespace Avalonia.Markup.Xaml.Data anchor, enableDataValidation); } - else if (RelativeSource.Mode == RelativeSourceMode.Self) + else if (relativeSource.Mode == RelativeSourceMode.Self) { observer = CreateSourceObserver(target, pathInfo.Path, enableDataValidation); } - else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent) + else if (relativeSource.Mode == RelativeSourceMode.TemplatedParent) { observer = CreateTemplatedParentObserver(target, pathInfo.Path); } - else if (RelativeSource.Mode == RelativeSourceMode.FindAncestor) + else if (relativeSource.Mode == RelativeSourceMode.FindAncestor) { - if (RelativeSource.AncestorType == null) + if (relativeSource.AncestorType == null) { throw new InvalidOperationException("AncestorType must be set for RelativeSourceModel.FindAncestor."); } observer = CreateFindAncestorObserver( (target as IControl) ?? (anchor as IControl), + relativeSource, pathInfo.Path); } else @@ -187,6 +191,66 @@ namespace Avalonia.Markup.Xaml.Data result.ElementName = path.Substring(1); } } + else if (path.StartsWith("$")) + { + var relativeSource = new RelativeSource(); + result.RelativeSource = relativeSource; + var dot = path.IndexOf('.'); + string relativeSourceMode; + if (dot != -1) + { + result.Path = path.Substring(dot + 1); + relativeSourceMode = path.Substring(1, dot - 1); + } + else + { + result.Path = string.Empty; + relativeSourceMode = path.Substring(1); + } + + if (relativeSourceMode == "self") + { + relativeSource.Mode = RelativeSourceMode.Self; + } + else if(relativeSourceMode == "parent") + { + relativeSource.Mode = RelativeSourceMode.FindAncestor; + var parentConfigStart = relativeSourceMode.IndexOf('['); + if (parentConfigStart != -1) + { + if (!relativeSourceMode.EndsWith("]")) + { + throw new InvalidOperationException("Invalid RelativeSource binding syntax. Expected matching ']' for '['."); + } + var parentConfigParams = relativeSourceMode.Substring(0, relativeSourceMode.Length - 1).Split(','); + if (parentConfigParams.Length > 2 || parentConfigParams.Length == 0) + { + throw new InvalidOperationException("Expected either 1 or 2 parameters for RelativeSource binding syntax"); + } + else if (parentConfigParams.Length == 1) + { + if (int.TryParse(parentConfigParams[0], out int level)) + { + relativeSource.AncestorType = typeof(IControl); + relativeSource.AncestorLevel = level; + } + else + { + relativeSource.AncestorType = LookupAncestorType(parentConfigParams[0]); + } + } + else + { + relativeSource.AncestorType = LookupAncestorType(parentConfigParams[0]); + relativeSource.AncestorLevel = int.Parse(parentConfigParams[1]); + } + } + } + else + { + throw new InvalidOperationException($"Invalid RelativeSource binding syntax: {relativeSourceMode}"); + } + } else { result.Path = path; @@ -195,6 +259,12 @@ namespace Avalonia.Markup.Xaml.Data return result; } + private static Type LookupAncestorType(string ancestorTypeName) + { + //TODO: What is our syntax for type lookup here? + throw new NotImplementedException(); + } + private void ValidateState(PathInfo pathInfo) { if (pathInfo.ElementName != null && ElementName != null) @@ -203,8 +273,14 @@ namespace Avalonia.Markup.Xaml.Data "ElementName property cannot be set when an #elementName path is provided."); } + if (pathInfo.RelativeSource != null && RelativeSource != null) + { + throw new InvalidOperationException( + "ElementName property cannot be set when a $self or $parent path is provided."); + } + if ((pathInfo.ElementName != null || ElementName != null) && - RelativeSource != null) + (pathInfo.RelativeSource != null || RelativeSource != null)) { throw new InvalidOperationException( "ElementName property cannot be set with a RelativeSource."); @@ -267,12 +343,13 @@ namespace Avalonia.Markup.Xaml.Data private ExpressionObserver CreateFindAncestorObserver( IControl target, + RelativeSource relativeSource, string path) { Contract.Requires(target != null); return new ExpressionObserver( - ControlLocator.Track(target, RelativeSource.AncestorType, RelativeSource.AncestorLevel -1), + ControlLocator.Track(target, relativeSource.AncestorType, relativeSource.AncestorLevel -1), path); } @@ -324,6 +401,7 @@ namespace Avalonia.Markup.Xaml.Data { public string Path { get; set; } public string ElementName { get; set; } + public RelativeSource RelativeSource { get; set; } } } } \ No newline at end of file From bf78e35489c8e5a7c6a7091f4657abd7e2756d3b Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Mon, 9 Oct 2017 17:43:10 -0500 Subject: [PATCH 08/61] Update XUnit to a released version. --- build/XUnit.props | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/build/XUnit.props b/build/XUnit.props index 42fe9b6d7a..1d7da7257d 100644 --- a/build/XUnit.props +++ b/build/XUnit.props @@ -1,14 +1,14 @@  - + - - - - - - - + + + + + + + From a29837dd042b8ee068daef5049d1883cfe3d7189 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Mon, 9 Oct 2017 17:44:45 -0500 Subject: [PATCH 09/61] Implement syntax sugar for RelativeSource (not including setting a specific AncestorType) --- .../Avalonia.Markup.Xaml/Data/Binding.cs | 47 +++++++++-------- .../Xaml/BindingTests_RelativeSource.cs | 50 +++++++++++++++++++ 2 files changed, 75 insertions(+), 22 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs index 9d911999c6..052f743a4b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs @@ -96,7 +96,7 @@ namespace Avalonia.Markup.Xaml.Data ValidateState(pathInfo); enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue; - var elementName = pathInfo.ElementName; + var elementName = ElementName ?? pathInfo.ElementName; var relativeSource = RelativeSource ?? pathInfo.RelativeSource; ExpressionObserver observer; @@ -213,38 +213,41 @@ namespace Avalonia.Markup.Xaml.Data relativeSource.Mode = RelativeSourceMode.Self; } else if(relativeSourceMode == "parent") + { + relativeSource.Mode = RelativeSourceMode.FindAncestor; + relativeSource.AncestorType = typeof(IControl); + relativeSource.AncestorLevel = 1; + } + else if(relativeSourceMode.StartsWith("parent[")) { relativeSource.Mode = RelativeSourceMode.FindAncestor; var parentConfigStart = relativeSourceMode.IndexOf('['); - if (parentConfigStart != -1) + if (!relativeSourceMode.EndsWith("]")) { - if (!relativeSourceMode.EndsWith("]")) - { - throw new InvalidOperationException("Invalid RelativeSource binding syntax. Expected matching ']' for '['."); - } - var parentConfigParams = relativeSourceMode.Substring(0, relativeSourceMode.Length - 1).Split(','); - if (parentConfigParams.Length > 2 || parentConfigParams.Length == 0) - { - throw new InvalidOperationException("Expected either 1 or 2 parameters for RelativeSource binding syntax"); - } - else if (parentConfigParams.Length == 1) + throw new InvalidOperationException("Invalid RelativeSource binding syntax. Expected matching ']' for '['."); + } + var parentConfigParams = relativeSourceMode.Substring(parentConfigStart + 1).TrimEnd(']').Split(','); + if (parentConfigParams.Length > 2 || parentConfigParams.Length == 0) + { + throw new InvalidOperationException("Expected either 1 or 2 parameters for RelativeSource binding syntax"); + } + else if (parentConfigParams.Length == 1) + { + if (int.TryParse(parentConfigParams[0], out int level)) { - if (int.TryParse(parentConfigParams[0], out int level)) - { - relativeSource.AncestorType = typeof(IControl); - relativeSource.AncestorLevel = level; - } - else - { - relativeSource.AncestorType = LookupAncestorType(parentConfigParams[0]); - } + relativeSource.AncestorType = typeof(IControl); + relativeSource.AncestorLevel = level + 1; } else { relativeSource.AncestorType = LookupAncestorType(parentConfigParams[0]); - relativeSource.AncestorLevel = int.Parse(parentConfigParams[1]); } } + else + { + relativeSource.AncestorType = LookupAncestorType(parentConfigParams[0]); + relativeSource.AncestorLevel = int.Parse(parentConfigParams[1]) + 1; + } } else { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs index 9125e1e643..a4e4d4f5c3 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs @@ -77,6 +77,31 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [Fact] + public void Binding_To_First_Ancestor_With_Shorthand_Works() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + +