From b519ecc121220a972dcc20585e5d36e75cba8252 Mon Sep 17 00:00:00 2001 From: OronDF343 Date: Fri, 31 May 2019 10:14:28 +0300 Subject: [PATCH 01/43] Support StringFormat without Converter in MultiBinding --- .../StringFormatMultiValueConverter.cs | 46 +++++++++++++++++++ .../Avalonia.Markup/Data/MultiBinding.cs | 23 ++++------ .../Data/MultiBindingTests_Converters.cs | 40 ++++++++++++---- 3 files changed, 86 insertions(+), 23 deletions(-) create mode 100644 src/Avalonia.Base/Data/Converters/StringFormatMultiValueConverter.cs diff --git a/src/Avalonia.Base/Data/Converters/StringFormatMultiValueConverter.cs b/src/Avalonia.Base/Data/Converters/StringFormatMultiValueConverter.cs new file mode 100644 index 0000000000..b190f06be5 --- /dev/null +++ b/src/Avalonia.Base/Data/Converters/StringFormatMultiValueConverter.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Avalonia.Data.Converters +{ + /// + /// A multi-value converter which calls + /// + public class StringFormatMultiValueConverter : IMultiValueConverter + { + /// + /// Initializes a new instance of the class. + /// + /// The format string. + /// + /// An optional inner converter to be called before the format takes place. + /// + public StringFormatMultiValueConverter(string format, IMultiValueConverter inner) + { + Contract.Requires(format != null); + + Format = format; + Inner = inner; + } + + /// + /// Gets an inner value converter which will be called before the string format takes place. + /// + public IMultiValueConverter Inner { get; } + + /// + /// Gets the format string. + /// + public string Format { get; } + + /// + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + return Inner == null + ? string.Format(culture, Format, values.ToArray()) + : string.Format(culture, Format, Inner.Convert(values, targetType, parameter, culture)); + } + } +} diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index eb2a8df2eb..45e4739eb0 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -64,16 +64,20 @@ namespace Avalonia.Data object anchor = null, bool enableDataValidation = false) { - if (Converter == null) - { - throw new NotSupportedException("MultiBinding without Converter not currently supported."); - } - var targetType = targetProperty?.PropertyType ?? typeof(object); var children = Bindings.Select(x => x.Initiate(target, null)); var input = children.Select(x => x.Observable).CombineLatest().Select(x => ConvertValue(x, targetType)); var mode = Mode == BindingMode.Default ? targetProperty?.GetMetadata(target.GetType()).DefaultBindingMode : Mode; + + // We only respect `StringFormat` if the type of the property we're assigning to will + // accept a string. Note that this is slightly different to WPF in that WPF only applies + // `StringFormat` for target type `string` (not `object`). + if (!string.IsNullOrWhiteSpace(StringFormat) && + (targetType == typeof(string) || targetType == typeof(object))) + { + Converter = new StringFormatMultiValueConverter(StringFormat, Converter); + } switch (mode) { @@ -97,15 +101,6 @@ namespace Avalonia.Data converted = FallbackValue; } - // We only respect `StringFormat` if the type of the property we're assigning to will - // accept a string. Note that this is slightly different to WPF in that WPF only applies - // `StringFormat` for target type `string` (not `object`). - if (!string.IsNullOrWhiteSpace(StringFormat) && - (targetType == typeof(string) || targetType == typeof(object))) - { - converted = string.Format(culture, StringFormat, converted); - } - return converted; } } diff --git a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests_Converters.cs b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests_Converters.cs index bd4b5b9d04..d3e3ce5507 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests_Converters.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests_Converters.cs @@ -5,11 +5,10 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Text; using Avalonia.Controls; using Avalonia.Data; using Avalonia.Data.Converters; -using Avalonia.Data.Core; +using Avalonia.Layout; using Xunit; namespace Avalonia.Markup.UnitTests.Data @@ -21,7 +20,30 @@ namespace Avalonia.Markup.UnitTests.Data { var textBlock = new TextBlock { - DataContext = new MultiBindingTests_Converters.Class1(), + DataContext = new Class1(), + }; + + var target = new MultiBinding + { + StringFormat = "{0:0.0} + {1:00}", + Bindings = + { + new Binding(nameof(Class1.Foo)), + new Binding(nameof(Class1.Bar)), + } + }; + + textBlock.Bind(TextBlock.TextProperty, target); + + Assert.Equal("1.0 + 02", textBlock.Text); + } + + [Fact] + public void StringFormat_Should_Be_Applied_After_Converter() + { + var textBlock = new TextBlock + { + DataContext = new Class1(), }; var target = new MultiBinding @@ -30,8 +52,8 @@ namespace Avalonia.Markup.UnitTests.Data Converter = new SumOfDoublesConverter(), Bindings = { - new Binding(nameof(MultiBindingTests_Converters.Class1.Foo)), - new Binding(nameof(MultiBindingTests_Converters.Class1.Bar)), + new Binding(nameof(Class1.Foo)), + new Binding(nameof(Class1.Bar)), } }; @@ -45,7 +67,7 @@ namespace Avalonia.Markup.UnitTests.Data { var textBlock = new TextBlock { - DataContext = new MultiBindingTests_Converters.Class1(), + DataContext = new Class1(), }; var target = new MultiBinding @@ -54,12 +76,12 @@ namespace Avalonia.Markup.UnitTests.Data Converter = new SumOfDoublesConverter(), Bindings = { - new Binding(nameof(MultiBindingTests_Converters.Class1.Foo)), - new Binding(nameof(MultiBindingTests_Converters.Class1.Bar)), + new Binding(nameof(Class1.Foo)), + new Binding(nameof(Class1.Bar)), } }; - textBlock.Bind(TextBlock.WidthProperty, target); + textBlock.Bind(Layoutable.WidthProperty, target); Assert.Equal(3.0, textBlock.Width); } From d6b273a40cc89efcdb44cb45e472cdff79f082ac Mon Sep 17 00:00:00 2001 From: OronDF343 Date: Fri, 7 Jun 2019 12:05:53 +0300 Subject: [PATCH 02/43] Fixed StringFormat being applied multiple times --- src/Markup/Avalonia.Markup/Data/MultiBinding.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index 45e4739eb0..33d12e8eb5 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -65,19 +65,20 @@ namespace Avalonia.Data bool enableDataValidation = false) { var targetType = targetProperty?.PropertyType ?? typeof(object); - var children = Bindings.Select(x => x.Initiate(target, null)); - var input = children.Select(x => x.Observable).CombineLatest().Select(x => ConvertValue(x, targetType)); - var mode = Mode == BindingMode.Default ? - targetProperty?.GetMetadata(target.GetType()).DefaultBindingMode : Mode; - + var converter = Converter; // We only respect `StringFormat` if the type of the property we're assigning to will // accept a string. Note that this is slightly different to WPF in that WPF only applies // `StringFormat` for target type `string` (not `object`). if (!string.IsNullOrWhiteSpace(StringFormat) && (targetType == typeof(string) || targetType == typeof(object))) { - Converter = new StringFormatMultiValueConverter(StringFormat, Converter); + converter = new StringFormatMultiValueConverter(StringFormat, converter); } + + var children = Bindings.Select(x => x.Initiate(target, null)); + var input = children.Select(x => x.Observable).CombineLatest().Select(x => ConvertValue(x, targetType, converter)); + var mode = Mode == BindingMode.Default ? + targetProperty?.GetMetadata(target.GetType()).DefaultBindingMode : Mode; switch (mode) { @@ -91,10 +92,10 @@ namespace Avalonia.Data } } - private object ConvertValue(IList values, Type targetType) + private object ConvertValue(IList values, Type targetType, IMultiValueConverter converter) { var culture = CultureInfo.CurrentCulture; - var converted = Converter.Convert(values, targetType, ConverterParameter, culture); + var converted = converter.Convert(values, targetType, ConverterParameter, culture); if (converted == AvaloniaProperty.UnsetValue && FallbackValue != null) { From 433c70a6c826af2bf65fad745a4ff160502deab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Sun, 9 Jun 2019 21:26:08 +0100 Subject: [PATCH 03/43] Added BindingOperations.DoNothing. Added tests. --- src/Avalonia.Base/Data/BindingOperations.cs | 2 + .../Data/Core/BindingExpression.cs | 31 ++++++++ .../Avalonia.Markup/Data/MultiBinding.cs | 7 +- .../Converters/ValueConverterTests.cs | 71 +++++++++++++++++++ .../Data/BindingTests.cs | 20 +++--- 5 files changed, 118 insertions(+), 13 deletions(-) create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/Converters/ValueConverterTests.cs diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index 15de6c4d0d..44b47329ac 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -10,6 +10,8 @@ namespace Avalonia.Data { public static class BindingOperations { + public static readonly object DoNothing = new object(); + /// /// Applies an a property on an . /// diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index f1717bde3b..7f8396cdfa 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -114,6 +114,11 @@ namespace Avalonia.Data.Core /// public void OnNext(object value) { + if (value == BindingOperations.DoNothing) + { + return; + } + using (_inner.Subscribe(_ => { })) { var type = _inner.ResultType; @@ -126,6 +131,11 @@ namespace Avalonia.Data.Core ConverterParameter, CultureInfo.CurrentCulture); + if (converted == BindingOperations.DoNothing) + { + return; + } + if (converted == AvaloniaProperty.UnsetValue) { converted = TypeUtilities.Default(type); @@ -186,6 +196,11 @@ namespace Avalonia.Data.Core /// private object ConvertValue(object value) { + if (value == BindingOperations.DoNothing) + { + return value; + } + var notification = value as BindingNotification; if (notification == null) @@ -196,6 +211,11 @@ namespace Avalonia.Data.Core ConverterParameter, CultureInfo.CurrentCulture); + if (converted == BindingOperations.DoNothing) + { + return converted; + } + notification = converted as BindingNotification; if (notification?.ErrorType == BindingErrorType.None) @@ -327,7 +347,18 @@ namespace Avalonia.Data.Core public void OnNext(object value) { + if (value == BindingOperations.DoNothing) + { + return; + } + var converted = _owner.ConvertValue(value); + + if (converted == BindingOperations.DoNothing) + { + return; + } + _owner._value = new WeakReference(converted); _owner.PublishNext(converted); } diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index eb2a8df2eb..d1e3765dfa 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -92,7 +92,12 @@ namespace Avalonia.Data var culture = CultureInfo.CurrentCulture; var converted = Converter.Convert(values, targetType, ConverterParameter, culture); - if (converted == AvaloniaProperty.UnsetValue && FallbackValue != null) + if (converted == BindingOperations.DoNothing) + { + return converted; + } + + if (converted == AvaloniaProperty.UnsetValue) { converted = FallbackValue; } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ValueConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ValueConverterTests.cs new file mode 100644 index 0000000000..6f2c4363e2 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ValueConverterTests.cs @@ -0,0 +1,71 @@ +using System; +using System.Globalization; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Converters +{ + public class ValueConverterTests + { + [Fact] + public void ValueConverter_Special_Values_Work() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + window.ApplyTemplate(); + + window.DataContext = 2; + Assert.Equal("foo", textBlock.Text); + + window.DataContext = -3; + Assert.Equal("foo", textBlock.Text); + + window.DataContext = 0; + Assert.Equal("bar", textBlock.Text); + } + } + } + + public class TestConverter : IValueConverter + { + public static readonly TestConverter Instance = new TestConverter(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is int i) + { + if (i > 0) + { + return "foo"; + } + + if (i == 0) + { + return AvaloniaProperty.UnsetValue; + } + + return BindingOperations.DoNothing; + } + + return "(default)"; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs index 0a00b9d39d..9813c1f222 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs @@ -1,20 +1,10 @@ // 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; -using System.Collections.Generic; -using System.Linq; -using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Data; -using Avalonia.Markup.Data; -using Moq; -using Xunit; -using System.ComponentModel; -using System.Runtime.CompilerServices; using Avalonia.UnitTests; -using Avalonia.Data.Converters; -using Avalonia.Data.Core; +using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.Data { @@ -34,10 +24,16 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data var window = (Window)loader.Load(xaml); var textBlock = window.FindControl("textBlock"); - window.DataContext = "foo"; window.ApplyTemplate(); + window.DataContext = "foo"; Assert.Equal("foo", textBlock.Text); + + window.DataContext = BindingOperations.DoNothing; + Assert.Equal("foo", textBlock.Text); + + window.DataContext = "bar"; + Assert.Equal("bar", textBlock.Text); } } } From 687037ab50ad9d0cc62e0272a3b4468f002c631e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 10 Jun 2019 19:35:09 +0200 Subject: [PATCH 04/43] Initial port of WPF DockPanel --- src/Avalonia.Controls/DockPanel.cs | 164 +++++++++++++++++------------ 1 file changed, 97 insertions(+), 67 deletions(-) diff --git a/src/Avalonia.Controls/DockPanel.cs b/src/Avalonia.Controls/DockPanel.cs index e147fe1a52..e580283d85 100644 --- a/src/Avalonia.Controls/DockPanel.cs +++ b/src/Avalonia.Controls/DockPanel.cs @@ -70,107 +70,137 @@ namespace Avalonia.Controls set { SetValue(LastChildFillProperty, value); } } - /// + /// + /// Updates DesiredSize of the DockPanel. Called by parent Control. This is the first pass of layout. + /// + /// + /// Children are measured based on their sizing properties and . + /// Each child is allowed to consume all of the space on the side on which it is docked; Left/Right docked + /// children are granted all vertical space for their entire width, and Top/Bottom docked children are + /// granted all horizontal space for their entire height. + /// + /// Constraint size is an "upper limit" that the return value should not exceed. + /// The Panel's desired size. protected override Size MeasureOverride(Size constraint) { - double usedWidth = 0.0; - double usedHeight = 0.0; - double maximumWidth = 0.0; - double maximumHeight = 0.0; + var children = Children; - // Measure each of the Children - foreach (Control element in Children) + double parentWidth = 0; // Our current required width due to children thus far. + double parentHeight = 0; // Our current required height due to children thus far. + double accumulatedWidth = 0; // Total width consumed by children. + double accumulatedHeight = 0; // Total height consumed by children. + + for (int i = 0, count = children.Count; i < count; ++i) { - // Get the child's desired size - Size remainingSize = new Size( - Math.Max(0.0, constraint.Width - usedWidth), - Math.Max(0.0, constraint.Height - usedHeight)); - element.Measure(remainingSize); - Size desiredSize = element.DesiredSize; - - // Decrease the remaining space for the rest of the children - switch (GetDock(element)) + var child = children[i]; + Size childConstraint; // Contains the suggested input constraint for this child. + Size childDesiredSize; // Contains the return size from child measure. + + if (child == null) + { continue; } + + // Child constraint is the remaining size; this is total size minus size consumed by previous children. + childConstraint = new Size(Math.Max(0.0, constraint.Width - accumulatedWidth), + Math.Max(0.0, constraint.Height - accumulatedHeight)); + + // Measure child. + child.Measure(childConstraint); + childDesiredSize = child.DesiredSize; + + // Now, we adjust: + // 1. Size consumed by children (accumulatedSize). This will be used when computing subsequent + // children to determine how much space is remaining for them. + // 2. Parent size implied by this child (parentSize) when added to the current children (accumulatedSize). + // This is different from the size above in one respect: A Dock.Left child implies a height, but does + // not actually consume any height for subsequent children. + // If we accumulate size in a given dimension, the next child (or the end conditions after the child loop) + // will deal with computing our minimum size (parentSize) due to that accumulation. + // Therefore, we only need to compute our minimum size (parentSize) in dimensions that this child does + // not accumulate: Width for Top/Bottom, Height for Left/Right. + switch (DockPanel.GetDock((Control)child)) { case Dock.Left: case Dock.Right: - maximumHeight = Math.Max(maximumHeight, usedHeight + desiredSize.Height); - usedWidth += desiredSize.Width; + parentHeight = Math.Max(parentHeight, accumulatedHeight + childDesiredSize.Height); + accumulatedWidth += childDesiredSize.Width; break; + case Dock.Top: case Dock.Bottom: - maximumWidth = Math.Max(maximumWidth, usedWidth + desiredSize.Width); - usedHeight += desiredSize.Height; + parentWidth = Math.Max(parentWidth, accumulatedWidth + childDesiredSize.Width); + accumulatedHeight += childDesiredSize.Height; break; } } - maximumWidth = Math.Max(maximumWidth, usedWidth); - maximumHeight = Math.Max(maximumHeight, usedHeight); - return new Size(maximumWidth, maximumHeight); + // Make sure the final accumulated size is reflected in parentSize. + parentWidth = Math.Max(parentWidth, accumulatedWidth); + parentHeight = Math.Max(parentHeight, accumulatedHeight); + + return (new Size(parentWidth, parentHeight)); } - /// + /// + /// DockPanel computes a position and final size for each of its children based upon their + /// enum and sizing properties. + /// + /// Size that DockPanel will assume to position children. protected override Size ArrangeOverride(Size arrangeSize) { - double left = 0.0; - double top = 0.0; - double right = 0.0; - double bottom = 0.0; - - // Arrange each of the Children var children = Children; - int dockedCount = children.Count - (LastChildFill ? 1 : 0); - int index = 0; + int totalChildrenCount = children.Count; + int nonFillChildrenCount = totalChildrenCount - (LastChildFill ? 1 : 0); - foreach (Control element in children) + double accumulatedLeft = 0; + double accumulatedTop = 0; + double accumulatedRight = 0; + double accumulatedBottom = 0; + + for (int i = 0; i < totalChildrenCount; ++i) { - // Determine the remaining space left to arrange the element - Rect remainingRect = new Rect( - left, - top, - Math.Max(0.0, arrangeSize.Width - left - right), - Math.Max(0.0, arrangeSize.Height - top - bottom)); - - // Trim the remaining Rect to the docked size of the element - // (unless the element should fill the remaining space because - // of LastChildFill) - if (index < dockedCount) + var child = children[i]; + if (child == null) + { continue; } + + Size childDesiredSize = child.DesiredSize; + Rect rcChild = new Rect( + accumulatedLeft, + accumulatedTop, + Math.Max(0.0, arrangeSize.Width - (accumulatedLeft + accumulatedRight)), + Math.Max(0.0, arrangeSize.Height - (accumulatedTop + accumulatedBottom))); + + if (i < nonFillChildrenCount) { - Size desiredSize = element.DesiredSize; - switch (GetDock(element)) + switch (DockPanel.GetDock((Control)child)) { case Dock.Left: - left += desiredSize.Width; - remainingRect = remainingRect.WithWidth(desiredSize.Width); - break; - case Dock.Top: - top += desiredSize.Height; - remainingRect = remainingRect.WithHeight(desiredSize.Height); + accumulatedLeft += childDesiredSize.Width; + rcChild = rcChild.WithWidth(childDesiredSize.Width); break; + case Dock.Right: - right += desiredSize.Width; - remainingRect = new Rect( - Math.Max(0.0, arrangeSize.Width - right), - remainingRect.Y, - desiredSize.Width, - remainingRect.Height); + accumulatedRight += childDesiredSize.Width; + rcChild = rcChild.WithX(Math.Max(0.0, arrangeSize.Width - accumulatedRight)); + rcChild = rcChild.WithWidth(childDesiredSize.Width); break; + + case Dock.Top: + accumulatedTop += childDesiredSize.Height; + rcChild = rcChild.WithHeight(childDesiredSize.Height); + break; + case Dock.Bottom: - bottom += desiredSize.Height; - remainingRect = new Rect( - remainingRect.X, - Math.Max(0.0, arrangeSize.Height - bottom), - remainingRect.Width, - desiredSize.Height); + accumulatedBottom += childDesiredSize.Height; + rcChild = rcChild.WithY(Math.Max(0.0, arrangeSize.Height - accumulatedBottom)); + rcChild = rcChild.WithHeight(childDesiredSize.Height); break; } } - element.Arrange(remainingRect); - index++; + child.Arrange(rcChild); } - return arrangeSize; + return (arrangeSize); } } } From 1c2e27015523cbe13ce0aa40a02340d32d27d9c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 10 Jun 2019 21:25:27 +0200 Subject: [PATCH 05/43] Initial port of WPF StackPanel --- src/Avalonia.Controls/StackPanel.cs | 136 +++++++++++++++------------- 1 file changed, 73 insertions(+), 63 deletions(-) diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index c29faa1b4d..d456e462f5 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -2,9 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Linq; -using Avalonia.Input; -using Avalonia.Layout; +using Avalonia.Input namespace Avalonia.Controls { @@ -155,106 +153,118 @@ namespace Avalonia.Controls } /// - /// Measures the control. + /// General StackPanel layout behavior is to grow unbounded in the "stacking" direction (Size To Content). + /// Children in this dimension are encouraged to be as large as they like. In the other dimension, + /// StackPanel will assume the maximum size of its children. /// - /// The available size. - /// The desired size of the control. - protected override Size MeasureOverride(Size availableSize) + /// Constraint + /// Desired size + protected override Size MeasureOverride(Size constraint) { - double childAvailableWidth = double.PositiveInfinity; - double childAvailableHeight = double.PositiveInfinity; + Size stackDesiredSize = new Size(); + var children = Children; + Size layoutSlotSize = constraint; + bool fHorizontal = (Orientation == Orientation.Horizontal); + double spacing = Spacing; + bool hasVisibleChild = false; - if (Orientation == Orientation.Vertical) + // + // Initialize child sizing and iterator data + // Allow children as much size as they want along the stack. + // + if (fHorizontal) { - childAvailableWidth = availableSize.Width; - - if (!double.IsNaN(Width)) - { - childAvailableWidth = Width; - } - - childAvailableWidth = Math.Min(childAvailableWidth, MaxWidth); - childAvailableWidth = Math.Max(childAvailableWidth, MinWidth); + layoutSlotSize = layoutSlotSize.WithWidth(Double.PositiveInfinity); } else { - childAvailableHeight = availableSize.Height; + layoutSlotSize = layoutSlotSize.WithHeight(Double.PositiveInfinity); + } - if (!double.IsNaN(Height)) - { - childAvailableHeight = Height; - } + // + // Iterate through children. + // While we still supported virtualization, this was hidden in a child iterator (see source history). + // + for (int i = 0, count = children.Count; i < count; ++i) + { + // Get next child. + var child = children[i]; - childAvailableHeight = Math.Min(childAvailableHeight, MaxHeight); - childAvailableHeight = Math.Max(childAvailableHeight, MinHeight); - } + if (child == null) + { continue; } - double measuredWidth = 0; - double measuredHeight = 0; - double spacing = Spacing; - bool hasVisibleChild = Children.Any(c => c.IsVisible); + if (child.IsVisible) + { + hasVisibleChild = true; + } - foreach (Control child in Children) - { - child.Measure(new Size(childAvailableWidth, childAvailableHeight)); - Size size = child.DesiredSize; + // Measure the child. + child.Measure(layoutSlotSize); + Size childDesiredSize = child.DesiredSize; - if (Orientation == Orientation.Vertical) + // Accumulate child size. + if (fHorizontal) { - measuredHeight += size.Height + (child.IsVisible ? spacing : 0); - measuredWidth = Math.Max(measuredWidth, size.Width); + stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width + childDesiredSize.Width + (child.IsVisible ? spacing : 0)); + stackDesiredSize = stackDesiredSize.WithHeight(Math.Max(stackDesiredSize.Height, childDesiredSize.Height)); } else { - measuredWidth += size.Width + (child.IsVisible ? spacing : 0); - measuredHeight = Math.Max(measuredHeight, size.Height); + stackDesiredSize = stackDesiredSize.WithWidth(Math.Max(stackDesiredSize.Width, childDesiredSize.Width)); + stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height + childDesiredSize.Height + (child.IsVisible ? spacing : 0)); } } if (Orientation == Orientation.Vertical) { - measuredHeight -= (hasVisibleChild ? spacing : 0); + stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height - (hasVisibleChild ? spacing : 0)); } else { - measuredWidth -= (hasVisibleChild ? spacing : 0); + stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width - (hasVisibleChild ? spacing : 0)); } - return new Size(measuredWidth, measuredHeight).Constrain(availableSize); + return stackDesiredSize; } - /// + /// + /// Content arrangement. + /// + /// Arrange size protected override Size ArrangeOverride(Size finalSize) { - var orientation = Orientation; + var children = Children; + bool fHorizontal = (Orientation == Orientation.Horizontal); + Rect rcChild = new Rect(finalSize); + double previousChildSize = 0.0; var spacing = Spacing; - var finalRect = new Rect(finalSize); - var pos = 0.0; - foreach (Control child in Children) + // + // Arrange and Position Children. + // + for (int i = 0, count = children.Count; i < count; ++i) { - if (!child.IsVisible) - { - continue; - } + var child = children[i]; - double childWidth = child.DesiredSize.Width; - double childHeight = child.DesiredSize.Height; + if (child == null) + { continue; } - if (orientation == Orientation.Vertical) + if (fHorizontal) { - var rect = new Rect(0, pos, childWidth, childHeight) - .Align(finalRect, child.HorizontalAlignment, VerticalAlignment.Top); - ArrangeChild(child, rect, finalSize, orientation); - pos += childHeight + spacing; + rcChild = rcChild.WithX(rcChild.X + previousChildSize); + previousChildSize = child.DesiredSize.Width; + rcChild = rcChild.WithWidth(previousChildSize); + rcChild = rcChild.WithHeight(Math.Max(finalSize.Height, child.DesiredSize.Height) + (child.IsVisible ? spacing : 0)); } else { - var rect = new Rect(pos, 0, childWidth, childHeight) - .Align(finalRect, HorizontalAlignment.Left, child.VerticalAlignment); - ArrangeChild(child, rect, finalSize, orientation); - pos += childWidth + spacing; + rcChild = rcChild.WithY(rcChild.Y + previousChildSize); + previousChildSize = child.DesiredSize.Height; + rcChild = rcChild.WithHeight(previousChildSize); + rcChild = rcChild.WithWidth(Math.Max(finalSize.Width, child.DesiredSize.Width) + (child.IsVisible ? spacing : 0)); } + + child.Arrange(rcChild); } return finalSize; From 8304eaab8502ac0e288b7c31bd40e721df7f8a34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 10 Jun 2019 21:26:14 +0200 Subject: [PATCH 06/43] Fix --- src/Avalonia.Controls/StackPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index d456e462f5..6caffb8686 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using Avalonia.Input +using Avalonia.Input; namespace Avalonia.Controls { From 39a696b9851e46dacd5c6b6293ec0da14f0169fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 10 Jun 2019 22:12:36 +0200 Subject: [PATCH 07/43] Fix tests --- src/Avalonia.Controls/StackPanel.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index 6caffb8686..0a64096ee9 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -215,13 +215,13 @@ namespace Avalonia.Controls } } - if (Orientation == Orientation.Vertical) + if (fHorizontal) { - stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height - (hasVisibleChild ? spacing : 0)); + stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width - (hasVisibleChild ? spacing : 0)); } else - { - stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width - (hasVisibleChild ? spacing : 0)); + { + stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height - (hasVisibleChild ? spacing : 0)); } return stackDesiredSize; @@ -252,16 +252,16 @@ namespace Avalonia.Controls if (fHorizontal) { rcChild = rcChild.WithX(rcChild.X + previousChildSize); - previousChildSize = child.DesiredSize.Width; + previousChildSize = child.DesiredSize.Width + (child.IsVisible ? spacing : 0); rcChild = rcChild.WithWidth(previousChildSize); - rcChild = rcChild.WithHeight(Math.Max(finalSize.Height, child.DesiredSize.Height) + (child.IsVisible ? spacing : 0)); + rcChild = rcChild.WithHeight(Math.Max(finalSize.Height, child.DesiredSize.Height)); } else { rcChild = rcChild.WithY(rcChild.Y + previousChildSize); - previousChildSize = child.DesiredSize.Height; + previousChildSize = child.DesiredSize.Height + (child.IsVisible ? spacing : 0); rcChild = rcChild.WithHeight(previousChildSize); - rcChild = rcChild.WithWidth(Math.Max(finalSize.Width, child.DesiredSize.Width) + (child.IsVisible ? spacing : 0)); + rcChild = rcChild.WithWidth(Math.Max(finalSize.Width, child.DesiredSize.Width)); } child.Arrange(rcChild); From 895f458fa9b5fb23c6c86389cee8abad3e8fe677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 10 Jun 2019 22:20:21 +0200 Subject: [PATCH 08/43] Update StackPanel.cs --- src/Avalonia.Controls/StackPanel.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index 0a64096ee9..c2f36b94d7 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -157,13 +157,13 @@ namespace Avalonia.Controls /// Children in this dimension are encouraged to be as large as they like. In the other dimension, /// StackPanel will assume the maximum size of its children. /// - /// Constraint + /// Constraint /// Desired size - protected override Size MeasureOverride(Size constraint) + protected override Size MeasureOverride(Size availableSize) { Size stackDesiredSize = new Size(); var children = Children; - Size layoutSlotSize = constraint; + Size layoutSlotSize = availableSize; bool fHorizontal = (Orientation == Orientation.Horizontal); double spacing = Spacing; bool hasVisibleChild = false; @@ -224,7 +224,7 @@ namespace Avalonia.Controls stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height - (hasVisibleChild ? spacing : 0)); } - return stackDesiredSize; + return stackDesiredSize.Constrain(availableSize); // TODO: In WPF `.Constrain(availableSize)` is not used. } /// From 705ebc26752a1b9e956cda8ac5dd908eb1883e59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 10 Jun 2019 22:30:37 +0200 Subject: [PATCH 09/43] Update StackPanel.cs --- src/Avalonia.Controls/StackPanel.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index c2f36b94d7..fd75353364 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -205,13 +205,13 @@ namespace Avalonia.Controls // Accumulate child size. if (fHorizontal) { - stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width + childDesiredSize.Width + (child.IsVisible ? spacing : 0)); + stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width + (child.IsVisible ? spacing : 0) + childDesiredSize.Width); stackDesiredSize = stackDesiredSize.WithHeight(Math.Max(stackDesiredSize.Height, childDesiredSize.Height)); } else { stackDesiredSize = stackDesiredSize.WithWidth(Math.Max(stackDesiredSize.Width, childDesiredSize.Width)); - stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height + childDesiredSize.Height + (child.IsVisible ? spacing : 0)); + stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height + (child.IsVisible ? spacing : 0) + childDesiredSize.Height); } } @@ -224,7 +224,9 @@ namespace Avalonia.Controls stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height - (hasVisibleChild ? spacing : 0)); } - return stackDesiredSize.Constrain(availableSize); // TODO: In WPF `.Constrain(availableSize)` is not used. + return stackDesiredSize; + // TODO: In WPF `.Constrain(availableSize)` is not used. + //return stackDesiredSize.Constrain(availableSize); } /// @@ -251,15 +253,15 @@ namespace Avalonia.Controls if (fHorizontal) { - rcChild = rcChild.WithX(rcChild.X + previousChildSize); - previousChildSize = child.DesiredSize.Width + (child.IsVisible ? spacing : 0); + rcChild = rcChild.WithX(rcChild.X + previousChildSize + spacing); + previousChildSize = child.DesiredSize.Width;//+ (child.IsVisible ? spacing : 0); rcChild = rcChild.WithWidth(previousChildSize); rcChild = rcChild.WithHeight(Math.Max(finalSize.Height, child.DesiredSize.Height)); } else { - rcChild = rcChild.WithY(rcChild.Y + previousChildSize); - previousChildSize = child.DesiredSize.Height + (child.IsVisible ? spacing : 0); + rcChild = rcChild.WithY(rcChild.Y + previousChildSize + spacing); + previousChildSize = child.DesiredSize.Height;//+ (child.IsVisible ? spacing : 0); rcChild = rcChild.WithHeight(previousChildSize); rcChild = rcChild.WithWidth(Math.Max(finalSize.Width, child.DesiredSize.Width)); } From af69f801204435e46ea7312f334f46ea79e7f652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 10 Jun 2019 23:08:21 +0200 Subject: [PATCH 10/43] Update StackPanel.cs --- src/Avalonia.Controls/StackPanel.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index fd75353364..a4e711bbd3 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -193,7 +193,9 @@ namespace Avalonia.Controls if (child == null) { continue; } - if (child.IsVisible) + bool isVisible = child.IsVisible; + + if (isVisible && !hasVisibleChild) { hasVisibleChild = true; } @@ -205,13 +207,13 @@ namespace Avalonia.Controls // Accumulate child size. if (fHorizontal) { - stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width + (child.IsVisible ? spacing : 0) + childDesiredSize.Width); + stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width + (isVisible ? spacing : 0) + childDesiredSize.Width); stackDesiredSize = stackDesiredSize.WithHeight(Math.Max(stackDesiredSize.Height, childDesiredSize.Height)); } else { stackDesiredSize = stackDesiredSize.WithWidth(Math.Max(stackDesiredSize.Width, childDesiredSize.Width)); - stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height + (child.IsVisible ? spacing : 0) + childDesiredSize.Height); + stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height + (isVisible ? spacing : 0) + childDesiredSize.Height); } } From 4cf502e61b5ccf409d84f72cc5f0acd50cee5bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 10 Jun 2019 23:15:54 +0200 Subject: [PATCH 11/43] Update StackPanel.cs --- src/Avalonia.Controls/StackPanel.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index a4e711bbd3..8abc82f747 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -255,17 +255,19 @@ namespace Avalonia.Controls if (fHorizontal) { - rcChild = rcChild.WithX(rcChild.X + previousChildSize + spacing); - previousChildSize = child.DesiredSize.Width;//+ (child.IsVisible ? spacing : 0); + rcChild = rcChild.WithX(rcChild.X + previousChildSize); + previousChildSize = child.DesiredSize.Width; rcChild = rcChild.WithWidth(previousChildSize); rcChild = rcChild.WithHeight(Math.Max(finalSize.Height, child.DesiredSize.Height)); + previousChildSize += spacing; } else { - rcChild = rcChild.WithY(rcChild.Y + previousChildSize + spacing); - previousChildSize = child.DesiredSize.Height;//+ (child.IsVisible ? spacing : 0); + rcChild = rcChild.WithY(rcChild.Y + previousChildSize); + previousChildSize = child.DesiredSize.Height; rcChild = rcChild.WithHeight(previousChildSize); rcChild = rcChild.WithWidth(Math.Max(finalSize.Width, child.DesiredSize.Width)); + previousChildSize += spacing; } child.Arrange(rcChild); From ebb8d43727f6688db9c69851b033600e46f75715 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 10 Jun 2019 23:15:58 +0200 Subject: [PATCH 12/43] Update StackPanel.cs --- src/Avalonia.Controls/StackPanel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index 8abc82f747..8b002dc64d 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -226,9 +226,9 @@ namespace Avalonia.Controls stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height - (hasVisibleChild ? spacing : 0)); } - return stackDesiredSize; // TODO: In WPF `.Constrain(availableSize)` is not used. - //return stackDesiredSize.Constrain(availableSize); + //return stackDesiredSize; + return stackDesiredSize.Constrain(availableSize); } /// From 4ae22f579e47df6c7edf2d7ec93fc3b099f64d11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 12 Jun 2019 17:28:57 +0200 Subject: [PATCH 13/43] Fix license headers --- src/Avalonia.Controls/DockPanel.cs | 9 +++++++-- src/Avalonia.Controls/StackPanel.cs | 6 ++++-- src/Avalonia.Controls/WrapPanel.cs | 10 ++++------ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Controls/DockPanel.cs b/src/Avalonia.Controls/DockPanel.cs index e580283d85..8e23555c2d 100644 --- a/src/Avalonia.Controls/DockPanel.cs +++ b/src/Avalonia.Controls/DockPanel.cs @@ -1,7 +1,12 @@ +// This source file is adapted from the Windows Presentation Foundation project. +// (https://github.com/dotnet/wpf/) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; + namespace Avalonia.Controls { - using System; - /// /// Defines the available docking modes for a control in a . /// diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index 8b002dc64d..59c3c33942 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -1,5 +1,7 @@ -// 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. +// This source file is adapted from the Windows Presentation Foundation project. +// (https://github.com/dotnet/wpf/) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; using Avalonia.Input; diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index 4df1b39400..6f53d853c7 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -1,9 +1,7 @@ -// 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.Diagnostics; -using System.Linq; +// This source file is adapted from the Windows Presentation Foundation project. +// (https://github.com/dotnet/wpf/) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using Avalonia.Input; using Avalonia.Utilities; From 8515e027ef2074c1378061efde89b939be03bc68 Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 20 Jun 2019 11:43:04 +0300 Subject: [PATCH 14/43] Add AutoSuspendHelper implementation --- src/Avalonia.ReactiveUI/AutoSuspendHelper.cs | 44 ++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/Avalonia.ReactiveUI/AutoSuspendHelper.cs diff --git a/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs b/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs new file mode 100644 index 0000000000..e39ecb4a0f --- /dev/null +++ b/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs @@ -0,0 +1,44 @@ +// 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 Avalonia; +using Avalonia.VisualTree; +using Avalonia.Controls; +using System.Threading; +using System.Reactive.Disposables; +using System.Reactive.Subjects; +using System.Reactive.Linq; +using System.Reactive; +using ReactiveUI; +using System; + +namespace Avalonia.ReactiveUI +{ + /// + /// A ReactiveUI AutoSuspendHelper which initializes suspension hooks for + /// Avalonia applications. Call its constructor in your app's composition root, + /// before calling the RxApp.SuspensionHost.SetupDefaultSuspendResume method. + /// + public sealed class AutoSuspendHelper + { + public AutoSuspendHelper(Application app) + { + RxApp.SuspensionHost.IsResuming = Observable.Never(); + RxApp.SuspensionHost.IsLaunchingNew = Observable.Return(Unit.Default); + + var exiting = new Subject(); + app.Exit += (o, e) => + { + // This is required to prevent the app from shutting down too early. + var manual = new ManualResetEvent(false); + exiting.OnNext(Disposable.Create(() => manual.Set())); + manual.WaitOne(); + }; + RxApp.SuspensionHost.ShouldPersistState = exiting; + + var errored = new Subject(); + AppDomain.CurrentDomain.UnhandledException += (o, e) => errored.OnNext(Unit.Default); + RxApp.SuspensionHost.ShouldInvalidateState = errored; + } + } +} From 5230e465a663fee27dc9d05d562230073bcca669 Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 20 Jun 2019 12:03:33 +0300 Subject: [PATCH 15/43] Correct wrong variable name --- .../AvaloniaActivationForViewFetcherTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs index 2c81e8fea3..1d85312b1a 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs @@ -171,7 +171,7 @@ namespace Avalonia.ReactiveUI.UnitTests [Fact] public void Activation_For_View_Fetcher_Should_Support_Windows() { - using (var application = UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) { var window = new TestWindowWithWhenActivated(); Assert.False(window.Active); @@ -187,7 +187,7 @@ namespace Avalonia.ReactiveUI.UnitTests [Fact] public void Activatable_Window_View_Model_Is_Activated_And_Deactivated() { - using (var application = UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) { var viewModel = new ActivatableViewModel(); var window = new ActivatableWindow { ViewModel = viewModel }; From 6c16ac48032730a7b8ef1fd5bb701f251878028f Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 20 Jun 2019 12:38:59 +0300 Subject: [PATCH 16/43] Unit test AutoSuspendHelper --- .../AutoSuspendHelperTest.cs | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs new file mode 100644 index 0000000000..a3464878e6 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs @@ -0,0 +1,60 @@ +// 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; +using System.Reactive.Concurrency; +using System.Reactive.Disposables; +using Avalonia.Controls; +using Avalonia.Rendering; +using Avalonia.Platform; +using Avalonia.UnitTests; +using Avalonia; +using ReactiveUI; +using DynamicData; +using Xunit; +using Splat; +using Avalonia.Markup.Xaml; +using System.ComponentModel; +using System.Threading.Tasks; +using System.Reactive; +using Avalonia.ReactiveUI; +using System.Reactive.Subjects; +using System.Reactive.Linq; +using System.Collections.Generic; + +namespace Avalonia.ReactiveUI.UnitTests +{ + public class AutoSuspendHelperTest + { + [Fact] + public void AutoSuspendHelper_Should_Immediately_Fire_IsLaunchingNew() + { + using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + { + var isLaunchingReceived = false; + var application = AvaloniaLocator.Current.GetService(); + var suspension = new AutoSuspendHelper(application); + + RxApp.SuspensionHost.IsLaunchingNew.Subscribe(_ => isLaunchingReceived = true); + Assert.True(isLaunchingReceived); + } + } + + [Fact] + public void ShouldPersistState_Should_Fire_On_App_Exit_When_SuspensionDriver_Is_Initialized() + { + using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + { + var shouldPersistReceived = false; + var application = AvaloniaLocator.Current.GetService(); + var suspension = new AutoSuspendHelper(application); + + RxApp.SuspensionHost.ShouldPersistState.Subscribe(_ => shouldPersistReceived = true); + RxApp.SuspensionHost.SetupDefaultSuspendResume(new DummySuspensionDriver()); + + application.Shutdown(); + Assert.True(shouldPersistReceived); + } + } + } +} \ No newline at end of file From 2e9fbe238ba5c2c9dd82a3f1ff05a6a329787e02 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 20 Jun 2019 11:48:08 +0200 Subject: [PATCH 17/43] Refactor selection in SelectingItemsControl. Instead of using `SelectedItems` as the canonical source of selection information, use an internal `Selection` class which keeps track of the selection in terms of item indexes. Fixes #2004 --- .../Primitives/SelectingItemsControl.cs | 564 ++++++++++++------ .../Primitives/SelectingItemsControlTests.cs | 41 +- .../SelectingItemsControlTests_Multiple.cs | 302 +++++++++- 3 files changed, 677 insertions(+), 230 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 7b91d6235d..143b2ffd7d 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -12,6 +12,7 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; +using Avalonia.Logging; using Avalonia.Styling; using Avalonia.VisualTree; @@ -103,6 +104,7 @@ namespace Avalonia.Controls.Primitives private static readonly IList Empty = Array.Empty(); + private Selection _selection = new Selection(); private int _selectedIndex = -1; private object _selectedItem; private IList _selectedItems; @@ -152,23 +154,8 @@ namespace Avalonia.Controls.Primitives { if (_updateCount == 0) { - SetAndRaise(SelectedIndexProperty, ref _selectedIndex, (int val, ref int backing, Action notifyWrapper) => - { - var old = backing; - var effective = (val >= 0 && val < Items?.Cast().Count()) ? val : -1; - - if (old != effective) - { - backing = effective; - notifyWrapper(() => - RaisePropertyChanged( - SelectedIndexProperty, - old, - effective, - BindingPriority.LocalValue)); - SelectedItem = ElementAt(Items, effective); - } - }, value); + var effective = (value >= 0 && value < ItemCount) ? value : -1; + UpdateSelectedItem(effective); } else { @@ -192,41 +179,7 @@ namespace Avalonia.Controls.Primitives { if (_updateCount == 0) { - SetAndRaise(SelectedItemProperty, ref _selectedItem, (object val, ref object backing, Action notifyWrapper) => - { - var old = backing; - var index = IndexOf(Items, val); - var effective = index != -1 ? val : null; - - if (!object.Equals(effective, old)) - { - backing = effective; - - notifyWrapper(() => - RaisePropertyChanged( - SelectedItemProperty, - old, - effective, - BindingPriority.LocalValue)); - - SelectedIndex = index; - - if (effective != null) - { - if (SelectedItems.Count != 1 || SelectedItems[0] != effective) - { - _syncingSelectedItems = true; - SelectedItems.Clear(); - SelectedItems.Add(effective); - _syncingSelectedItems = false; - } - } - else if (SelectedItems.Count > 0) - { - SelectedItems.Clear(); - } - } - }, value); + UpdateSelectedItem(IndexOf(Items, value)); } else { @@ -439,11 +392,7 @@ namespace Avalonia.Controls.Primitives { if (i.ContainerControl != null && i.Item != null) { - var ms = MemberSelector; - bool selected = ms == null ? - SelectedItems.Contains(i.Item) : - SelectedItems.OfType().Any(v => Equals(ms.Select(v), i.Item)); - + bool selected = _selection.Contains(i.Index); MarkContainerSelected(i.ContainerControl, selected); } } @@ -476,9 +425,12 @@ namespace Avalonia.Controls.Primitives var keymap = AvaloniaLocator.Current.GetService(); bool Match(List gestures) => gestures.Any(g => g.Matches(e)); - if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll)) + if (ItemCount > 0 && + Match(keymap.SelectAll) && + (((SelectionMode & SelectionMode.Multiple) != 0) || + (SelectionMode & SelectionMode.Toggle) != 0)) { - SynchronizeItems(SelectedItems, Items?.Cast()); + SelectAll(); e.Handled = true; } } @@ -520,6 +472,52 @@ namespace Avalonia.Controls.Primitives return false; } + /// + /// Selects all items in the control. + /// + protected void SelectAll() + { + if ((SelectionMode & (SelectionMode.Multiple | SelectionMode.Toggle)) == 0) + { + throw new NotSupportedException("Multiple selection is not enabled on this control."); + } + + UpdateSelectedItems(() => + { + _selection.Clear(); + + for (var i = 0; i < ItemCount; ++i) + { + _selection.Add(i); + } + + foreach (var container in ItemContainerGenerator.Containers) + { + MarkItemSelected(container.Index, true); + } + + ResetSelectedItems(GetRange(Items, 0, ItemCount - 1)); + }); + } + + /// + /// Deselects all items in the control. + /// + protected void UnselectAll() + { + UpdateSelectedItems(() => + { + _selection.Clear(); + + foreach (var container in ItemContainerGenerator.Containers) + { + MarkItemSelected(container.Index, false); + } + + SelectedItems.Clear(); + }); + } + /// /// Updates the selection for an item based on user interaction. /// @@ -538,40 +536,66 @@ namespace Avalonia.Controls.Primitives if (select) { var mode = SelectionMode; - var toggle = toggleModifier || (mode & SelectionMode.Toggle) != 0; var multi = (mode & SelectionMode.Multiple) != 0; - var range = multi && SelectedIndex != -1 && rangeModifier; + var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0); + var range = multi && rangeModifier; - if (!toggle && !range) - { - SelectedIndex = index; - } - else if (multi && range) + if (range) { - SynchronizeItems( - SelectedItems, - GetRange(Items, SelectedIndex, index)); + UpdateSelectedItems(() => + { + var start = SelectedIndex != -1 ? SelectedIndex : 0; + var first = Math.Min(start, index); + var last = Math.Max(start, index); + + for (var i = first; i < last; ++i) + { + _selection.Add(i); + } + + _selection.Add(last); + + foreach (var container in ItemContainerGenerator.Containers) + { + MarkItemSelected( + container.Index, + container.Index >= first && container.Index <= last); + } + + ResetSelectedItems(GetRange(Items, start, index)); + }); } - else + else if (multi && toggle) { - var item = ElementAt(Items, index); - var i = SelectedItems.IndexOf(item); - - if (i != -1 && (!AlwaysSelected || SelectedItems.Count > 1)) + UpdateSelectedItems(() => { - SelectedItems.Remove(item); - } - else - { - if (multi) + if (!_selection.Contains(index)) { - SelectedItems.Add(item); + _selection.Add(index); + MarkItemSelected(index, true); + SelectedItems.Add(ElementAt(Items, index)); } else { - SelectedIndex = index; + _selection.Remove(index); + MarkItemSelected(index, false); + + if (index == _selectedIndex) + { + UpdateSelectedItem(_selection.First(), false); + } + + SelectedItems.Remove(ElementAt(Items, index)); } - } + }); + } + else if (toggle) + { + SelectedIndex = (SelectedIndex == index) ? -1 : index; + } + else + { + SelectedIndex = index; } if (Presenter?.Panel != null) @@ -640,34 +664,24 @@ namespace Avalonia.Controls.Primitives } /// - /// Makes a list of objects equal another. + /// Makes a list of objects equal another (though doesn't preserve order). /// /// The items collection. /// The desired items. internal static void SynchronizeItems(IList items, IEnumerable desired) { - var index = 0; + var list = items.Cast().ToList(); + var toRemove = list.Except(desired).ToList(); + var toAdd = desired.Except(list).ToList(); - foreach (object item in desired) + foreach(var i in toRemove) { - int itemIndex = items.IndexOf(item); - - if (itemIndex == -1) - { - items.Insert(index, item); - } - else if(itemIndex != index) - { - items.RemoveAt(itemIndex); - items.Insert(index, item); - } - - ++index; + items.Remove(i); } - while (index < items.Count) + foreach (var i in toAdd) { - items.RemoveAt(items.Count - 1); + items.Add(i); } } @@ -678,17 +692,19 @@ namespace Avalonia.Controls.Primitives /// The index of the first item. /// The index of the last item. /// The items. - private static IEnumerable GetRange(IEnumerable items, int first, int last) + private static List GetRange(IEnumerable items, int first, int last) { var list = (items as IList) ?? items.Cast().ToList(); - int step = first > last ? -1 : 1; + var step = first > last ? -1 : 1; + var result = new List(); for (int i = first; i != last; i += step) { - yield return list[i]; + result.Add(list[i]); } - yield return list[last]; + result.Add(list[last]); + return result; } /// @@ -724,19 +740,14 @@ namespace Avalonia.Controls.Primitives private void LostSelection() { var items = Items?.Cast(); + var index = -1; if (items != null && AlwaysSelected) { - var index = Math.Min(SelectedIndex, items.Count() - 1); - - if (index > -1) - { - SelectedItem = items.ElementAt(index); - return; - } + index = Math.Min(SelectedIndex, items.Count() - 1); } - SelectedIndex = -1; + SelectedIndex = index; } /// @@ -793,7 +804,7 @@ namespace Avalonia.Controls.Primitives /// /// The item. /// Whether the item should be selected or deselected. - private void MarkItemSelected(object item, bool selected) + private int MarkItemSelected(object item, bool selected) { var index = IndexOf(Items, item); @@ -801,6 +812,18 @@ namespace Avalonia.Controls.Primitives { MarkItemSelected(index, selected); } + + return index; + } + + private void ResetSelectedItems(IEnumerable items) + { + SelectedItems.Clear(); + + foreach (var i in items) + { + SelectedItems.Add(i); + } } /// @@ -810,95 +833,97 @@ namespace Avalonia.Controls.Primitives /// The event args. private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { - var generator = ItemContainerGenerator; + if (_syncingSelectedItems) + { + return; + } + + void Add(IList newItems, IList addedItems = null) + { + foreach (var item in newItems) + { + var index = MarkItemSelected(item, true); + + if (index != -1 && _selection.Add(index) && addedItems != null) + { + addedItems.Add(item); + } + } + } + + void UpdateSelection() + { + if ((SelectedIndex != -1 && !_selection.Contains(SelectedIndex)) || + (SelectedIndex == -1 && _selection.HasItems)) + { + _selectedIndex = _selection.First(); + _selectedItem = ElementAt(Items, _selectedIndex); + RaisePropertyChanged(SelectedIndexProperty, -1, _selectedIndex, BindingPriority.LocalValue); + RaisePropertyChanged(SelectedItemProperty, null, _selectedItem, BindingPriority.LocalValue); + + if (AutoScrollToSelectedItem) + { + ScrollIntoView(_selectedIndex); + } + } + } + IList added = null; IList removed = null; switch (e.Action) { case NotifyCollectionChangedAction.Add: - SelectedItemsAdded(e.NewItems.Cast().ToList()); - - if (AutoScrollToSelectedItem) { - ScrollIntoView(e.NewItems[0]); + Add(e.NewItems); + UpdateSelection(); + added = e.NewItems; } - added = e.NewItems; break; case NotifyCollectionChangedAction.Remove: if (SelectedItems.Count == 0) { - if (!_syncingSelectedItems) - { - SelectedIndex = -1; - } + SelectedIndex = -1; } foreach (var item in e.OldItems) { - MarkItemSelected(item, false); + var index = MarkItemSelected(item, false); + _selection.Remove(index); } removed = e.OldItems; break; + case NotifyCollectionChangedAction.Replace: + throw new NotSupportedException("Replacing items in a SelectedItems collection is not supported."); + + case NotifyCollectionChangedAction.Move: + throw new NotSupportedException("Moving items in a SelectedItems collection is not supported."); + case NotifyCollectionChangedAction.Reset: - if (generator != null) { removed = new List(); + added = new List(); - foreach (var item in generator.Containers) + foreach (var index in _selection.ToList()) { - if (item?.ContainerControl != null) + var item = ElementAt(Items, index); + + if (!SelectedItems.Contains(item)) { - if (MarkContainerSelected(item.ContainerControl, false)) - { - removed.Add(item.Item); - } + MarkItemSelected(index, false); + removed.Add(item); + _selection.Remove(index); } } - } - - if (SelectedItems.Count > 0) - { - _selectedItem = null; - SelectedItemsAdded(SelectedItems); - added = SelectedItems; - } - else if (!_syncingSelectedItems) - { - SelectedIndex = -1; - } - - break; - - case NotifyCollectionChangedAction.Replace: - foreach (var item in e.OldItems) - { - MarkItemSelected(item, false); - } - - foreach (var item in e.NewItems) - { - MarkItemSelected(item, true); - } - if (SelectedItem != SelectedItems[0] && !_syncingSelectedItems) - { - var oldItem = SelectedItem; - var oldIndex = SelectedIndex; - var item = SelectedItems[0]; - var index = IndexOf(Items, item); - _selectedIndex = index; - _selectedItem = item; - RaisePropertyChanged(SelectedIndexProperty, oldIndex, index, BindingPriority.LocalValue); - RaisePropertyChanged(SelectedItemProperty, oldItem, item, BindingPriority.LocalValue); + Add(SelectedItems, added); + UpdateSelection(); } - added = e.NewItems; - removed = e.OldItems; break; } @@ -912,34 +937,6 @@ namespace Avalonia.Controls.Primitives } } - /// - /// Called when items are added to the collection. - /// - /// The added items. - private void SelectedItemsAdded(IList items) - { - if (items.Count > 0) - { - foreach (var item in items) - { - MarkItemSelected(item, true); - } - - if (SelectedItem == null && !_syncingSelectedItems) - { - var index = IndexOf(Items, items[0]); - - if (index != -1) - { - _selectedItem = items[0]; - _selectedIndex = index; - RaisePropertyChanged(SelectedIndexProperty, -1, index, BindingPriority.LocalValue); - RaisePropertyChanged(SelectedItemProperty, null, items[0], BindingPriority.LocalValue); - } - } - } - } - /// /// Subscribes to the CollectionChanged event, if any. /// @@ -970,6 +967,130 @@ namespace Avalonia.Controls.Primitives } } + /// + /// Updates the selection due to a change to or + /// . + /// + /// The new selected index. + /// Whether to clear existing selection. + private void UpdateSelectedItem(int index, bool clear = true) + { + var oldIndex = _selectedIndex; + var oldItem = _selectedItem; + + if (index == -1 && AlwaysSelected) + { + index = Math.Min(SelectedIndex, ItemCount - 1); + } + + var item = ElementAt(Items, index); + var added = -1; + HashSet removed = null; + + _selectedIndex = index; + _selectedItem = item; + + if (oldIndex != index || _selection.HasMultiple) + { + if (clear) + { + removed = _selection.Clear(); + } + + if (index != -1) + { + if (_selection.Add(index)) + { + added = index; + } + + if (removed?.Contains(index) == true) + { + removed.Remove(index); + added = -1; + } + } + + if (removed != null) + { + foreach (var i in removed) + { + MarkItemSelected(i, false); + } + } + + MarkItemSelected(index, true); + + RaisePropertyChanged( + SelectedIndexProperty, + oldIndex, + index); + } + + if (!Equals(item, oldItem)) + { + RaisePropertyChanged( + SelectedItemProperty, + oldItem, + item); + + UpdateSelectedItems(() => + { + if (clear) + { + SelectedItems.Clear(); + + if (index != -1) + { + SelectedItems.Add(item); + } + } + else + { + if (added != -1) + { + SelectedItems.Add(added); + } + } + }); + } + + if (removed != null && index != -1) + { + removed.Remove(index); + } + + if (added != -1 || removed?.Count > 0) + { + var e = new SelectionChangedEventArgs( + SelectionChangedEvent, + added != -1 ? new[] { ElementAt(Items, added) } : Array.Empty(), + removed?.Select(x => ElementAt(Items, x)).ToArray() ?? Array.Empty()); + RaiseEvent(e); + } + } + + private void UpdateSelectedItems(Action action) + { + try + { + _syncingSelectedItems = true; + action(); + } + catch (Exception ex) + { + Logger.Error( + LogArea.Property, + this, + "Error thrown updating SelectedItems: {Error}", + ex); + } + finally + { + _syncingSelectedItems = false; + } + } + private void UpdateFinished() { if (_updateSelectedIndex != int.MinValue) @@ -981,5 +1102,56 @@ namespace Avalonia.Controls.Primitives SelectedItems = _updateSelectedItems; } } + + private class Selection : IEnumerable + { + private List _list = new List(); + private HashSet _set = new HashSet(); + + public bool HasItems => _set.Count > 0; + public bool HasMultiple => _set.Count > 1; + + public bool Add(int index) + { + if (index == -1) + { + throw new ArgumentException("Invalid index", "index"); + } + + if (_set.Add(index)) + { + _list.Add(index); + return true; + } + + return false; + } + + public bool Remove(int index) + { + if (_set.Remove(index)) + { + _list.RemoveAll(x => x == index); + return true; + } + + return false; + } + + public HashSet Clear() + { + var result = _set; + _list.Clear(); + _set = new HashSet(); + return result; + } + + public bool Contains(int index) => _set.Contains(index); + + public int First() => HasItems ? _list[0] : -1; + + public IEnumerator GetEnumerator() => _set.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 8e421bf0a2..683b3b41e8 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -536,6 +536,9 @@ namespace Avalonia.Controls.UnitTests.Primitives SelectedIndex = 1, }; + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + var called = false; target.SelectionChanged += (s, e) => @@ -545,8 +548,6 @@ namespace Avalonia.Controls.UnitTests.Primitives called = true; }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); target.SelectedIndex = -1; Assert.True(called); @@ -783,6 +784,42 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(2, vm.Child.SelectedIndex); } + [Fact] + public void Should_Select_Correct_Item_When_Duplicate_Items_Are_Present() + { + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz"}, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Down((Interactive)target.Presenter.Panel.Children[3]); + + var panel = target.Presenter.Panel; + + Assert.Equal(3, target.SelectedIndex); + } + + [Fact] + public void Should_Apply_Selected_Pseudoclass_To_Correct_Item_When_Duplicate_Items_Are_Present() + { + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Down((Interactive)target.Presenter.Panel.Children[3]); + + var panel = target.Presenter.Panel; + + Assert.Equal(new[] { ":selected" }, target.Presenter.Panel.Children[3].Classes); + } + private FuncControlTemplate Template() { return new FuncControlTemplate(control => diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 9ef7d567c8..e0a8fa81f4 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -10,6 +10,8 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Markup.Data; using Xunit; @@ -17,6 +19,8 @@ namespace Avalonia.Controls.UnitTests.Primitives { public class SelectingItemsControlTests_Multiple { + private MouseTestHelper _helper = new MouseTestHelper(); + [Fact] public void Setting_SelectedIndex_Should_Add_To_SelectedItems() { @@ -258,31 +262,25 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void Replacing_First_SelectedItem_Should_Update_SelectedItem_SelectedIndex() + public void Setting_SelectedIndex_Should_Unmark_Previously_Selected_Containers() { - var items = new[] - { - new ListBoxItem(), - new ListBoxItem(), - new ListBoxItem(), - }; - var target = new TestSelector { - Items = items, + Items = new[] { "foo", "bar", "baz" }, Template = Template(), }; target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - target.SelectedIndex = 1; - target.SelectedItems[0] = items[2]; - Assert.Equal(2, target.SelectedIndex); - Assert.Equal(items[2], target.SelectedItem); - Assert.False(items[0].IsSelected); - Assert.False(items[1].IsSelected); - Assert.True(items[2].IsSelected); + target.SelectedItems.Add("foo"); + target.SelectedItems.Add("bar"); + + Assert.Equal(new[] { 0, 1 }, SelectedContainers(target)); + + target.SelectedIndex = 2; + + Assert.Equal(new[] { 2 }, SelectedContainers(target)); } [Fact] @@ -361,6 +359,52 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(new[] { "baz", "qux", "qiz" }, target.SelectedItems.Cast().ToList()); } + [Fact] + public void Setting_SelectedIndex_After_Range_Should_Unmark_Previously_Selected_Containers() + { + var target = new TestSelector + { + Items = new[] { "foo", "bar", "baz", "qux" }, + Template = Template(), + SelectedIndex = 0, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + target.SelectRange(2); + + Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target)); + + target.SelectedIndex = 3; + + Assert.Equal(new[] { 3 }, SelectedContainers(target)); + } + + [Fact] + public void Toggling_Selection_After_Range_Should_Work() + { + var target = new TestSelector + { + Items = new[] { "foo", "bar", "baz", "foo", "bar", "baz" }, + Template = Template(), + SelectedIndex = 0, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + target.SelectRange(3); + + Assert.Equal(new[] { 0, 1, 2, 3 }, SelectedContainers(target)); + + target.Toggle(4); + + Assert.Equal(new[] { 0, 1, 2, 3, 4 }, SelectedContainers(target)); + } + [Fact] public void Suprious_SelectedIndex_Changes_Should_Not_Be_Triggered() { @@ -382,6 +426,40 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(new[] { -1, 1, 0 }, selectedIndexes); } + [Fact] + public void Can_Set_SelectedIndex_To_Another_Selected_Item() + { + var target = new TestSelector + { + Items = new[] { "foo", "bar", "baz" }, + Template = Template(), + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.SelectedItems.Add("foo"); + target.SelectedItems.Add("bar"); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal(new[] { "foo", "bar" }, target.SelectedItems); + Assert.Equal(new[] { 0, 1 }, SelectedContainers(target)); + + var raised = false; + target.SelectionChanged += (s, e) => + { + raised = true; + Assert.Empty(e.AddedItems); + Assert.Equal(new[] { "foo" }, e.RemovedItems); + }; + + target.SelectedIndex = 1; + + Assert.True(raised); + Assert.Equal(1, target.SelectedIndex); + Assert.Equal(new[] { "bar" }, target.SelectedItems); + Assert.Equal(new[] { 1 }, SelectedContainers(target)); + } + /// /// Tests a problem discovered with ListBox with selection. /// @@ -471,6 +549,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { DataContext = items, Template = Template(), + Items = items, }; var called = false; @@ -540,35 +619,193 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.True(called); } + + [Fact] + public void Shift_Selecting_From_No_Selection_Selects_From_Start() + { + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Down((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.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 Replacing_SelectedItems_Should_Raise_SelectionChanged_With_CorrectItems() + public void Ctrl_Selecting_SelectedItem_With_Multiple_Selection_Active_Sets_SelectedItem_To_Next_Selection() { - var items = new[] { "foo", "bar", "baz" }; + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Qux" }, + SelectionMode = SelectionMode.Multiple, + }; - var target = new TestSelector + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Down((Interactive)target.Presenter.Panel.Children[1]); + _helper.Down((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control); + _helper.Down((Interactive)target.Presenter.Panel.Children[3], modifiers: InputModifiers.Control); + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); + Assert.Equal(new[] { "Bar", "Baz", "Qux" }, target.SelectedItems); + + _helper.Down((Interactive)target.Presenter.Panel.Children[1], modifiers: InputModifiers.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 { - Items = items, Template = Template(), - SelectedItem = "bar", + Items = new[] { "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, }; - var called = false; + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Down((Interactive)target.Presenter.Panel.Children[1]); + _helper.Down((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control); - target.SelectionChanged += (s, e) => + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); + + _helper.Down((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control); + + 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 { - Assert.Equal(new[] { "foo",}, e.AddedItems.Cast()); - Assert.Equal(new[] { "bar" }, e.RemovedItems.Cast()); - called = true; + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, }; target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - target.SelectedItems[0] = "foo"; + _helper.Down((Interactive)target.Presenter.Panel.Children[3]); + _helper.Down((Interactive)target.Presenter.Panel.Children[4], modifiers: InputModifiers.Control); - Assert.True(called); + 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 + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Down((Interactive)target.Presenter.Panel.Children[3]); + _helper.Down((Interactive)target.Presenter.Panel.Children[5], modifiers: InputModifiers.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 + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Down((Interactive)target.Presenter.Panel.Children[0]); + _helper.Down((Interactive)target.Presenter.Panel.Children[5], modifiers: InputModifiers.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 Duplicate_Items_Are_Added_To_SelectedItems_In_Order() + { + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Down((Interactive)target.Presenter.Panel.Children[0]); + + Assert.Equal(new[] { "Foo" }, target.SelectedItems); + + _helper.Down((Interactive)target.Presenter.Panel.Children[4], modifiers: InputModifiers.Control); + + Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); + + _helper.Down((Interactive)target.Presenter.Panel.Children[3], modifiers: InputModifiers.Control); + + Assert.Equal(new[] { "Foo", "Bar", "Foo" }, target.SelectedItems); + + _helper.Down((Interactive)target.Presenter.Panel.Children[1], modifiers: InputModifiers.Control); + + Assert.Equal(new[] { "Foo", "Bar", "Foo", "Bar" }, target.SelectedItems); + } + + [Fact] + public void SelectAll_Handles_Duplicate_Items() + { + var target = new TestSelector + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.SelectAll(); + + Assert.Equal(new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, target.SelectedItems); + } + + private IEnumerable SelectedContainers(SelectingItemsControl target) + { + return target.Presenter.Panel.Children + .Select((x, i) => x.Classes.Contains(":selected") ? i : -1) + .Where(x => x != -1); + } private FuncControlTemplate Template() { @@ -598,10 +835,11 @@ namespace Avalonia.Controls.UnitTests.Primitives set { base.SelectionMode = value; } } - public void SelectRange(int index) - { - UpdateSelection(index, true, true); - } + public new void SelectAll() => base.SelectAll(); + + public void SelectRange(int index) => UpdateSelection(index, true, true); + + public void Toggle(int index) => UpdateSelection(index, true, false, true); } private class OldDataContextViewModel From ac59468ddc935830092cd7100299abb07fbe026e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 20 Jun 2019 11:50:06 +0200 Subject: [PATCH 18/43] Add SelectAll and UnselectAll to ListBox. --- src/Avalonia.Controls/ListBox.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 041b81155a..3568183459 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -84,6 +84,16 @@ namespace Avalonia.Controls set { SetValue(VirtualizationModeProperty, value); } } + /// + /// Selects all items in the . + /// + public new void SelectAll() => base.SelectAll(); + + /// + /// Deselects all items in the . + /// + public new void UnselectAll() => base.UnselectAll(); + /// protected override IItemContainerGenerator CreateItemContainerGenerator() { From f58f8950bd784a9d096eef03cf811f1f8b6cebd5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 20 Jun 2019 11:56:55 +0200 Subject: [PATCH 19/43] Added failing test for #2574. --- .../Primitives/SelectingItemsControlTests.cs | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 683b3b41e8..3df66bb6cb 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -797,8 +797,6 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Presenter.ApplyTemplate(); _helper.Down((Interactive)target.Presenter.Panel.Children[3]); - var panel = target.Presenter.Panel; - Assert.Equal(3, target.SelectedIndex); } @@ -815,11 +813,35 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Presenter.ApplyTemplate(); _helper.Down((Interactive)target.Presenter.Panel.Children[3]); - var panel = target.Presenter.Panel; - Assert.Equal(new[] { ":selected" }, target.Presenter.Panel.Children[3].Classes); } + [Fact] + public void Adding_Item_Before_SelectedItem_Should_Update_SelectedIndex() + { + var items = new ObservableCollection + { + "Foo", + "Bar", + "Baz" + }; + + var target = new ListBox + { + Template = Template(), + Items = items, + SelectedIndex = 1, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + items.Insert(0, "Qux"); + + Assert.Equal(2, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); + } + private FuncControlTemplate Template() { return new FuncControlTemplate(control => From 6926f605f541b298eed56cd877948b6aea1d91e7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 20 Jun 2019 11:59:59 +0200 Subject: [PATCH 20/43] Update SelectedIndex when item added before selection. Fixes #2574. --- src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 143b2ffd7d..8cd714b1b8 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -307,6 +307,10 @@ namespace Avalonia.Controls.Primitives { SelectedIndex = 0; } + else if (e.NewStartingIndex <= SelectedIndex) + { + UpdateSelectedItem(SelectedIndex + e.NewItems.Count, false); + } break; From 1c85f409b3fa1e18cea40d714673f0dfdc00a0b3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 20 Jun 2019 12:16:18 +0200 Subject: [PATCH 21/43] Set SelectedIndex/Item on (Un)SelectAll(). --- .../Primitives/SelectingItemsControl.cs | 17 ++------ .../SelectingItemsControlTests_Multiple.cs | 43 ++++++++++++++++++- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 8cd714b1b8..c1db8e80ff 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -495,6 +495,8 @@ namespace Avalonia.Controls.Primitives _selection.Add(i); } + UpdateSelectedItem(0, false); + foreach (var container in ItemContainerGenerator.Containers) { MarkItemSelected(container.Index, true); @@ -507,20 +509,7 @@ namespace Avalonia.Controls.Primitives /// /// Deselects all items in the control. /// - protected void UnselectAll() - { - UpdateSelectedItems(() => - { - _selection.Clear(); - - foreach (var container in ItemContainerGenerator.Containers) - { - MarkItemSelected(container.Index, false); - } - - SelectedItems.Clear(); - }); - } + protected void UnselectAll() => UpdateSelectedItem(-1); /// /// Updates the selection for an item based on user interaction. diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index e0a8fa81f4..13dd5724c2 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Presenters; @@ -783,6 +784,45 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(new[] { "Foo", "Bar", "Foo", "Bar" }, target.SelectedItems); } + [Fact] + public void SelectAll_Sets_SelectedIndex_And_SelectedItem() + { + var target = new TestSelector + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + target.SelectAll(); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal("Foo", target.SelectedItem); + } + + [Fact] + public void UnselectAll_Clears_SelectedIndex_And_SelectedItem() + { + var target = new TestSelector + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + SelectedIndex = 0, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + target.UnselectAll(); + + Assert.Equal(-1, target.SelectedIndex); + Assert.Equal(null, target.SelectedItem); + } + [Fact] public void SelectAll_Handles_Duplicate_Items() { @@ -836,9 +876,8 @@ namespace Avalonia.Controls.UnitTests.Primitives } public new void SelectAll() => base.SelectAll(); - + public new void UnselectAll() => base.UnselectAll(); public void SelectRange(int index) => UpdateSelection(index, true, true); - public void Toggle(int index) => UpdateSelection(index, true, false, true); } From ca23d03f98ecde3cab421215e9b0996a8693c673 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 20 Jun 2019 12:27:38 +0200 Subject: [PATCH 22/43] Update SelectedIndex when items removed before selection. --- .../Primitives/SelectingItemsControl.cs | 14 ++++------ .../Primitives/SelectingItemsControlTests.cs | 26 +++++++++++++++++ .../SelectingItemsControlTests_Multiple.cs | 28 +++++++++++++++++++ 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index c1db8e80ff..080f09a4d3 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -316,26 +316,22 @@ namespace Avalonia.Controls.Primitives case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Replace: - var selectedIndex = SelectedIndex; - - if (selectedIndex >= e.OldStartingIndex && - selectedIndex < e.OldStartingIndex + e.OldItems.Count) + if (SelectedIndex >= e.OldStartingIndex && SelectedIndex < e.OldStartingIndex + e.OldItems.Count) { if (!AlwaysSelected) { - selectedIndex = SelectedIndex = -1; + SelectedIndex = -1; } else { LostSelection(); } } - - var items = Items?.Cast(); - if (selectedIndex >= items.Count()) + else if (e.OldStartingIndex <= SelectedIndex) { - selectedIndex = SelectedIndex = items.Count() - 1; + UpdateSelectedItem(SelectedIndex - e.OldItems.Count, false); } + break; case NotifyCollectionChangedAction.Move: diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 3df66bb6cb..4e7b7381cf 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -842,6 +842,32 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal("Bar", target.SelectedItem); } + [Fact] + public void Removing_Item_Before_SelectedItem_Should_Update_SelectedIndex() + { + var items = new ObservableCollection + { + "Foo", + "Bar", + "Baz" + }; + + var target = new ListBox + { + Template = Template(), + Items = items, + SelectedIndex = 1, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + items.RemoveAt(0); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); + } + private FuncControlTemplate Template() { return new FuncControlTemplate(control => diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 13dd5724c2..3639985140 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -840,6 +840,34 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, target.SelectedItems); } + [Fact] + public void Adding_Item_Before_SelectedItems_Should_Update_Indexes() + { + var items = new ObservableCollection + { + "Foo", + "Bar", + "Baz" + }; + + var target = new ListBox + { + Template = Template(), + Items = items, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + target.SelectAll(); + items.Insert(0, "Qux"); + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("Foo", target.SelectedItem); + } + + private IEnumerable SelectedContainers(SelectingItemsControl target) { return target.Presenter.Panel.Children From 94d71fb12febc7b95a04fb253a087365d14c4f50 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 20 Jun 2019 13:40:10 +0200 Subject: [PATCH 23/43] More work on updating selection... ...when source items change. --- .../Primitives/SelectingItemsControl.cs | 107 +++++++++++++----- .../Primitives/SelectingItemsControlTests.cs | 26 +++++ .../SelectingItemsControlTests_Multiple.cs | 91 ++++++++++++++- 3 files changed, 193 insertions(+), 31 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 080f09a4d3..78de2112f6 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -307,31 +307,23 @@ namespace Avalonia.Controls.Primitives { SelectedIndex = 0; } - else if (e.NewStartingIndex <= SelectedIndex) + else { - UpdateSelectedItem(SelectedIndex + e.NewItems.Count, false); + _selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count); + UpdateSelectedItem(_selection.First(), false); } break; case NotifyCollectionChangedAction.Remove: - case NotifyCollectionChangedAction.Replace: - if (SelectedIndex >= e.OldStartingIndex && SelectedIndex < e.OldStartingIndex + e.OldItems.Count) - { - if (!AlwaysSelected) - { - SelectedIndex = -1; - } - else - { - LostSelection(); - } - } - else if (e.OldStartingIndex <= SelectedIndex) - { - UpdateSelectedItem(SelectedIndex - e.OldItems.Count, false); - } + _selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count); + UpdateSelectedItem(_selection.First(), false); + ResetSelectedItems(); + break; + case NotifyCollectionChangedAction.Replace: + UpdateSelectedItem(SelectedIndex, false); + ResetSelectedItems(); break; case NotifyCollectionChangedAction.Move: @@ -498,7 +490,7 @@ namespace Avalonia.Controls.Primitives MarkItemSelected(container.Index, true); } - ResetSelectedItems(GetRange(Items, 0, ItemCount - 1)); + ResetSelectedItems(); }); } @@ -534,15 +526,19 @@ namespace Avalonia.Controls.Primitives UpdateSelectedItems(() => { var start = SelectedIndex != -1 ? SelectedIndex : 0; - var first = Math.Min(start, index); - var last = Math.Max(start, index); + var step = start < index ? 1 : -1; - for (var i = first; i < last; ++i) + _selection.Clear(); + + for (var i = start; i != index; i += step) { _selection.Add(i); } - _selection.Add(last); + _selection.Add(index); + + var first = Math.Min(start, index); + var last = Math.Max(start, index); foreach (var container in ItemContainerGenerator.Containers) { @@ -551,7 +547,7 @@ namespace Avalonia.Controls.Primitives container.Index >= first && container.Index <= last); } - ResetSelectedItems(GetRange(Items, start, index)); + ResetSelectedItems(); }); } else if (multi && toggle) @@ -805,14 +801,17 @@ namespace Avalonia.Controls.Primitives return index; } - private void ResetSelectedItems(IEnumerable items) + private void ResetSelectedItems() { - SelectedItems.Clear(); - - foreach (var i in items) + UpdateSelectedItems(() => { - SelectedItems.Add(i); - } + SelectedItems.Clear(); + + foreach (var i in _selection) + { + SelectedItems.Add(ElementAt(Items, i)); + } + }); } /// @@ -1135,6 +1134,54 @@ namespace Avalonia.Controls.Primitives return result; } + public void ItemsInserted(int index, int count) + { + _set = new HashSet(); + + for (var i = 0; i < _list.Count; ++i) + { + var ix = _list[i]; + + if (ix >= index) + { + var newIndex = ix + count; + _list[i] = newIndex; + _set.Add(newIndex); + } + else + { + _set.Add(ix); + } + } + } + + public void ItemsRemoved(int index, int count) + { + var last = (index + count) - 1; + + _set = new HashSet(); + + for (var i = 0; i < _list.Count; ++i) + { + var ix = _list[i]; + + if (ix >= index && ix <= last) + { + _list.RemoveAt(i--); + } + else if (ix > last) + { + var newIndex = ix - count; + _list[i] = newIndex; + _set.Add(newIndex); + } + else + { + _set.Add(ix); + } + } + } + public bool Contains(int index) => _set.Contains(index); public int First() => HasItems ? _list[0] : -1; diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 4e7b7381cf..75cababf54 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -868,6 +868,32 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal("Bar", target.SelectedItem); } + [Fact] + public void Replacing_Selected_Item_Should_Update_SelectedItem() + { + var items = new ObservableCollection + { + "Foo", + "Bar", + "Baz" + }; + + var target = new ListBox + { + Template = Template(), + Items = items, + SelectedIndex = 1, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + items[1] = "Qux"; + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("Qux", target.SelectedItem); + } + private FuncControlTemplate Template() { return new FuncControlTemplate(control => diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 3639985140..d8de98f4f2 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -841,7 +841,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void Adding_Item_Before_SelectedItems_Should_Update_Indexes() + public void Adding_Item_Before_SelectedItems_Should_Update_Selection() { var items = new ObservableCollection { @@ -865,8 +865,97 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(1, target.SelectedIndex); Assert.Equal("Foo", target.SelectedItem); + Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { 1, 2, 3 }, SelectedContainers(target)); + } + + [Fact] + public void Removing_Item_Before_SelectedItem_Should_Update_Selection() + { + var items = new ObservableCollection + { + "Foo", + "Bar", + "Baz" + }; + + var target = new TestSelector + { + Template = Template(), + Items = items, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + target.SelectedIndex = 1; + target.SelectRange(2); + + Assert.Equal(new[] { "Bar", "Baz" }, target.SelectedItems); + + items.RemoveAt(0); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); + Assert.Equal(new[] { "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { 0, 1 }, SelectedContainers(target)); + } + + [Fact] + public void Removing_SelectedItem_With_Multiple_Selection_Active_Should_Update_Selection() + { + var items = new ObservableCollection + { + "Foo", + "Bar", + "Baz" + }; + + var target = new ListBox + { + Template = Template(), + Items = items, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + target.SelectAll(); + items.RemoveAt(0); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); + Assert.Equal(new[] { "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { 0, 1 }, SelectedContainers(target)); } + [Fact] + public void Replacing_Selected_Item_Should_Update_SelectedItems() + { + var items = new ObservableCollection + { + "Foo", + "Bar", + "Baz" + }; + + var target = new ListBox + { + Template = Template(), + Items = items, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + target.SelectAll(); + items[1] = "Qux"; + + Assert.Equal(new[] { "Foo", "Qux", "Baz" }, target.SelectedItems); + } private IEnumerable SelectedContainers(SelectingItemsControl target) { From 1066f57c9e34eb61f4ce69efd1c0933efd0930be Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 20 Jun 2019 13:41:30 +0200 Subject: [PATCH 24/43] Move SynchronizeItems to TreeView. As it's no longer used in `SelectingItemsControl`. --- .../Primitives/SelectingItemsControl.cs | 22 ---------------- src/Avalonia.Controls/TreeView.cs | 26 +++++++++++++++++-- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 78de2112f6..269f662a62 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -648,28 +648,6 @@ namespace Avalonia.Controls.Primitives return false; } - /// - /// Makes a list of objects equal another (though doesn't preserve order). - /// - /// The items collection. - /// The desired items. - internal static void SynchronizeItems(IList items, IEnumerable desired) - { - var list = items.Cast().ToList(); - var toRemove = list.Except(desired).ToList(); - var toAdd = desired.Except(list).ToList(); - - foreach(var i in toRemove) - { - items.Remove(i); - } - - foreach (var i in toAdd) - { - items.Add(i); - } - } - /// /// Gets a range of items from an IEnumerable. /// diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index c3fbce1d83..888f4a2013 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -409,7 +409,7 @@ namespace Avalonia.Controls if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll)) { - SelectingItemsControl.SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items); + SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items); e.Handled = true; } } @@ -521,7 +521,7 @@ namespace Avalonia.Controls } else if (multi && range) { - SelectingItemsControl.SynchronizeItems( + SynchronizeItems( SelectedItems, GetItemsInRange(selectedContainer as TreeViewItem, container as TreeViewItem)); } @@ -778,5 +778,27 @@ namespace Avalonia.Controls container.Classes.Set(":selected", selected); } } + + /// + /// Makes a list of objects equal another (though doesn't preserve order). + /// + /// The items collection. + /// The desired items. + private static void SynchronizeItems(IList items, IEnumerable desired) + { + var list = items.Cast().ToList(); + var toRemove = list.Except(desired).ToList(); + var toAdd = desired.Except(list).ToList(); + + foreach (var i in toRemove) + { + items.Remove(i); + } + + foreach (var i in toAdd) + { + items.Add(i); + } + } } } From 561ae7f185728bbceab3624e36bfaf73c164a681 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 24 Jun 2019 19:17:23 +0800 Subject: [PATCH 25/43] Invalidate parent grid measure when Row/Col definition's height/width props change. Fixes #2664 --- src/Avalonia.Controls/ColumnDefinition.cs | 33 ++++++++++++++---- src/Avalonia.Controls/RowDefinition.cs | 41 +++++++++++++++++------ 2 files changed, 58 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls/ColumnDefinition.cs b/src/Avalonia.Controls/ColumnDefinition.cs index e3d2489241..9c520c434e 100644 --- a/src/Avalonia.Controls/ColumnDefinition.cs +++ b/src/Avalonia.Controls/ColumnDefinition.cs @@ -62,8 +62,15 @@ namespace Avalonia.Controls /// public double MaxWidth { - get { return GetValue(MaxWidthProperty); } - set { SetValue(MaxWidthProperty, value); } + get + { + return GetValue(MaxWidthProperty); + } + set + { + Parent?.InvalidateMeasure(); + SetValue(MaxWidthProperty, value); + } } /// @@ -71,8 +78,15 @@ namespace Avalonia.Controls /// public double MinWidth { - get { return GetValue(MinWidthProperty); } - set { SetValue(MinWidthProperty, value); } + get + { + return GetValue(MinWidthProperty); + } + set + { + Parent?.InvalidateMeasure(); + SetValue(MinWidthProperty, value); + } } /// @@ -80,8 +94,15 @@ namespace Avalonia.Controls /// public GridLength Width { - get { return GetValue(WidthProperty); } - set { SetValue(WidthProperty, value); } + get + { + return GetValue(WidthProperty); + } + set + { + Parent?.InvalidateMeasure(); + SetValue(WidthProperty, value); + } } internal override GridLength UserSizeValueCache => this.Width; diff --git a/src/Avalonia.Controls/RowDefinition.cs b/src/Avalonia.Controls/RowDefinition.cs index ad7312d515..1f2f738670 100644 --- a/src/Avalonia.Controls/RowDefinition.cs +++ b/src/Avalonia.Controls/RowDefinition.cs @@ -29,7 +29,7 @@ namespace Avalonia.Controls /// /// Initializes a new instance of the class. /// - public RowDefinition() + public RowDefinition() { } @@ -38,7 +38,7 @@ namespace Avalonia.Controls /// /// The height of the row. /// The height unit of the column. - public RowDefinition(double value, GridUnitType type) + public RowDefinition(double value, GridUnitType type) { Height = new GridLength(value, type); } @@ -47,7 +47,7 @@ namespace Avalonia.Controls /// Initializes a new instance of the class. /// /// The height of the column. - public RowDefinition(GridLength height) + public RowDefinition(GridLength height) { Height = height; } @@ -62,8 +62,15 @@ namespace Avalonia.Controls /// public double MaxHeight { - get { return GetValue(MaxHeightProperty); } - set { SetValue(MaxHeightProperty, value); } + get + { + return GetValue(MaxHeightProperty); + } + set + { + Parent?.InvalidateMeasure(); + SetValue(MaxHeightProperty, value); + } } /// @@ -71,8 +78,15 @@ namespace Avalonia.Controls /// public double MinHeight { - get { return GetValue(MinHeightProperty); } - set { SetValue(MinHeightProperty, value); } + get + { + return GetValue(MinHeightProperty); + } + set + { + Parent?.InvalidateMeasure(); + SetValue(MinHeightProperty, value); + } } /// @@ -80,12 +94,19 @@ namespace Avalonia.Controls /// public GridLength Height { - get { return GetValue(HeightProperty); } - set { SetValue(HeightProperty, value); } + get + { + return GetValue(HeightProperty); + } + set + { + Parent?.InvalidateMeasure(); + SetValue(HeightProperty, value); + } } internal override GridLength UserSizeValueCache => this.Height; internal override double UserMinSizeValueCache => this.MinHeight; internal override double UserMaxSizeValueCache => this.MaxHeight; } -} \ No newline at end of file +} From 710215ddb2558fe927880e95bae68485fd944cc1 Mon Sep 17 00:00:00 2001 From: wieslawsoltes Date: Tue, 25 Jun 2019 13:26:35 +0200 Subject: [PATCH 26/43] Revert StackPanel --- src/Avalonia.Controls/StackPanel.cs | 148 ++++++++++++---------------- 1 file changed, 65 insertions(+), 83 deletions(-) diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index 59c3c33942..c29faa1b4d 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -1,10 +1,10 @@ -// This source file is adapted from the Windows Presentation Foundation project. -// (https://github.com/dotnet/wpf/) -// -// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. +// 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; +using System.Linq; using Avalonia.Input; +using Avalonia.Layout; namespace Avalonia.Controls { @@ -155,124 +155,106 @@ namespace Avalonia.Controls } /// - /// General StackPanel layout behavior is to grow unbounded in the "stacking" direction (Size To Content). - /// Children in this dimension are encouraged to be as large as they like. In the other dimension, - /// StackPanel will assume the maximum size of its children. + /// Measures the control. /// - /// Constraint - /// Desired size + /// The available size. + /// The desired size of the control. protected override Size MeasureOverride(Size availableSize) { - Size stackDesiredSize = new Size(); - var children = Children; - Size layoutSlotSize = availableSize; - bool fHorizontal = (Orientation == Orientation.Horizontal); - double spacing = Spacing; - bool hasVisibleChild = false; + double childAvailableWidth = double.PositiveInfinity; + double childAvailableHeight = double.PositiveInfinity; - // - // Initialize child sizing and iterator data - // Allow children as much size as they want along the stack. - // - if (fHorizontal) + if (Orientation == Orientation.Vertical) { - layoutSlotSize = layoutSlotSize.WithWidth(Double.PositiveInfinity); + childAvailableWidth = availableSize.Width; + + if (!double.IsNaN(Width)) + { + childAvailableWidth = Width; + } + + childAvailableWidth = Math.Min(childAvailableWidth, MaxWidth); + childAvailableWidth = Math.Max(childAvailableWidth, MinWidth); } else { - layoutSlotSize = layoutSlotSize.WithHeight(Double.PositiveInfinity); - } + childAvailableHeight = availableSize.Height; - // - // Iterate through children. - // While we still supported virtualization, this was hidden in a child iterator (see source history). - // - for (int i = 0, count = children.Count; i < count; ++i) - { - // Get next child. - var child = children[i]; - - if (child == null) - { continue; } - - bool isVisible = child.IsVisible; - - if (isVisible && !hasVisibleChild) + if (!double.IsNaN(Height)) { - hasVisibleChild = true; + childAvailableHeight = Height; } - // Measure the child. - child.Measure(layoutSlotSize); - Size childDesiredSize = child.DesiredSize; + childAvailableHeight = Math.Min(childAvailableHeight, MaxHeight); + childAvailableHeight = Math.Max(childAvailableHeight, MinHeight); + } + + double measuredWidth = 0; + double measuredHeight = 0; + double spacing = Spacing; + bool hasVisibleChild = Children.Any(c => c.IsVisible); + + foreach (Control child in Children) + { + child.Measure(new Size(childAvailableWidth, childAvailableHeight)); + Size size = child.DesiredSize; - // Accumulate child size. - if (fHorizontal) + if (Orientation == Orientation.Vertical) { - stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width + (isVisible ? spacing : 0) + childDesiredSize.Width); - stackDesiredSize = stackDesiredSize.WithHeight(Math.Max(stackDesiredSize.Height, childDesiredSize.Height)); + measuredHeight += size.Height + (child.IsVisible ? spacing : 0); + measuredWidth = Math.Max(measuredWidth, size.Width); } else { - stackDesiredSize = stackDesiredSize.WithWidth(Math.Max(stackDesiredSize.Width, childDesiredSize.Width)); - stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height + (isVisible ? spacing : 0) + childDesiredSize.Height); + measuredWidth += size.Width + (child.IsVisible ? spacing : 0); + measuredHeight = Math.Max(measuredHeight, size.Height); } } - if (fHorizontal) + if (Orientation == Orientation.Vertical) { - stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width - (hasVisibleChild ? spacing : 0)); + measuredHeight -= (hasVisibleChild ? spacing : 0); } else - { - stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height - (hasVisibleChild ? spacing : 0)); + { + measuredWidth -= (hasVisibleChild ? spacing : 0); } - // TODO: In WPF `.Constrain(availableSize)` is not used. - //return stackDesiredSize; - return stackDesiredSize.Constrain(availableSize); + return new Size(measuredWidth, measuredHeight).Constrain(availableSize); } - /// - /// Content arrangement. - /// - /// Arrange size + /// protected override Size ArrangeOverride(Size finalSize) { - var children = Children; - bool fHorizontal = (Orientation == Orientation.Horizontal); - Rect rcChild = new Rect(finalSize); - double previousChildSize = 0.0; + var orientation = Orientation; var spacing = Spacing; + var finalRect = new Rect(finalSize); + var pos = 0.0; - // - // Arrange and Position Children. - // - for (int i = 0, count = children.Count; i < count; ++i) + foreach (Control child in Children) { - var child = children[i]; + if (!child.IsVisible) + { + continue; + } - if (child == null) - { continue; } + double childWidth = child.DesiredSize.Width; + double childHeight = child.DesiredSize.Height; - if (fHorizontal) + if (orientation == Orientation.Vertical) { - rcChild = rcChild.WithX(rcChild.X + previousChildSize); - previousChildSize = child.DesiredSize.Width; - rcChild = rcChild.WithWidth(previousChildSize); - rcChild = rcChild.WithHeight(Math.Max(finalSize.Height, child.DesiredSize.Height)); - previousChildSize += spacing; + var rect = new Rect(0, pos, childWidth, childHeight) + .Align(finalRect, child.HorizontalAlignment, VerticalAlignment.Top); + ArrangeChild(child, rect, finalSize, orientation); + pos += childHeight + spacing; } else { - rcChild = rcChild.WithY(rcChild.Y + previousChildSize); - previousChildSize = child.DesiredSize.Height; - rcChild = rcChild.WithHeight(previousChildSize); - rcChild = rcChild.WithWidth(Math.Max(finalSize.Width, child.DesiredSize.Width)); - previousChildSize += spacing; + var rect = new Rect(pos, 0, childWidth, childHeight) + .Align(finalRect, HorizontalAlignment.Left, child.VerticalAlignment); + ArrangeChild(child, rect, finalSize, orientation); + pos += childWidth + spacing; } - - child.Arrange(rcChild); } return finalSize; From 0225023d53d54f9ea794e02f38c6ae483ef15f2e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2019 14:54:22 +0200 Subject: [PATCH 27/43] Added failing tests for #2372. --- .../Primitives/RangeBaseTests.cs | 35 ++++++++++++++++++- .../Xaml/BasicTests.cs | 19 ++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs index 42578c61ac..0d88a1a9ff 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Markup.Data; using Avalonia.Styling; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Controls.UnitTests.Primitives @@ -160,6 +161,38 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(expected, track.Value); } + [Fact] + public void Coercion_Should_Not_Be_Done_During_Initialization() + { + var target = new TestRange(); + + target.BeginInit(); + + var root = new TestRoot(target); + target.Minimum = 1; + Assert.Equal(0, target.Value); + + target.Value = 50; + target.EndInit(); + + Assert.Equal(50, target.Value); + } + + [Fact] + public void Coercion_Should_Be_Done_After_Initialization() + { + var target = new TestRange(); + + target.BeginInit(); + + var root = new TestRoot(target); + target.Minimum = 1; + + target.EndInit(); + + Assert.Equal(1, target.Value); + } + private class TestRange : RangeBase { } @@ -199,4 +232,4 @@ namespace Avalonia.Controls.UnitTests.Primitives } } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index 7fe0fc4a08..720ff3b0de 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -907,6 +907,25 @@ do we need it?")] } } + [Fact] + public void Slider_Properties_Can_Be_Set_In_Any_Order() + { + using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + { + var xaml = @" + + +"; + + var window = AvaloniaXamlLoader.Parse(xaml); + var slider = (Slider)window.Content; + + Assert.Equal(0, slider.Minimum); + Assert.Equal(1000, slider.Maximum); + Assert.Equal(500, slider.Value); + } + } + private class SelectedItemsViewModel : INotifyPropertyChanged { public string[] Items { get; set; } From 4242c882b4daaca0a3580d27e6e04145d6c1eb21 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2019 14:59:28 +0200 Subject: [PATCH 28/43] Added StyledElement.OnInitialized. So that a control can easily react to its own initialization. --- src/Avalonia.Styling/StyledElement.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index f7e063dfb5..146a4c75e7 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -392,6 +392,7 @@ namespace Avalonia if (_initCount == 0 && !IsInitialized) { IsInitialized = true; + OnInitialized(); Initialized?.Invoke(this, EventArgs.Empty); } } @@ -608,7 +609,14 @@ namespace Avalonia protected virtual void OnDataContextEndUpdate() { } - + + /// + /// Called when the control finishes initialization. + /// + protected virtual void OnInitialized() + { + } + private static void DataContextNotifying(IAvaloniaObject o, bool updateStarted) { if (o is StyledElement element) From 440af4623550861e917c9ff62f668bc2af7997b9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2019 15:01:22 +0200 Subject: [PATCH 29/43] Don't coerce values until initialized. Fixes #2372. --- src/Avalonia.Controls/Primitives/RangeBase.cs | 65 ++++++++++++------- .../Primitives/RangeBaseTests.cs | 4 ++ 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/RangeBase.cs b/src/Avalonia.Controls/Primitives/RangeBase.cs index ae175734b9..f1ee7c0e1a 100644 --- a/src/Avalonia.Controls/Primitives/RangeBase.cs +++ b/src/Avalonia.Controls/Primitives/RangeBase.cs @@ -75,10 +75,18 @@ namespace Avalonia.Controls.Primitives set { - value = ValidateMinimum(value); - SetAndRaise(MinimumProperty, ref _minimum, value); - Maximum = ValidateMaximum(Maximum); - Value = ValidateValue(Value); + ValidateDouble(value, "Minimum"); + + if (IsInitialized) + { + SetAndRaise(MinimumProperty, ref _minimum, value); + Maximum = ValidateMaximum(Maximum); + Value = ValidateValue(Value); + } + else + { + SetAndRaise(MinimumProperty, ref _minimum, value); + } } } @@ -94,9 +102,18 @@ namespace Avalonia.Controls.Primitives set { - value = ValidateMaximum(value); - SetAndRaise(MaximumProperty, ref _maximum, value); - Value = ValidateValue(Value); + ValidateDouble(value, "Maximum"); + + if (IsInitialized) + { + value = ValidateMaximum(value); + SetAndRaise(MaximumProperty, ref _maximum, value); + Value = ValidateValue(Value); + } + else + { + SetAndRaise(MaximumProperty, ref _maximum, value); + } } } @@ -112,8 +129,17 @@ namespace Avalonia.Controls.Primitives set { - value = ValidateValue(value); - SetAndRaise(ValueProperty, ref _value, value); + ValidateDouble(value, "Value"); + + if (IsInitialized) + { + value = ValidateValue(value); + SetAndRaise(ValueProperty, ref _value, value); + } + else + { + SetAndRaise(ValueProperty, ref _value, value); + } } } @@ -129,6 +155,14 @@ namespace Avalonia.Controls.Primitives set => SetValue(LargeChangeProperty, value); } + protected override void OnInitialized() + { + base.OnInitialized(); + + Maximum = ValidateMaximum(Maximum); + Value = ValidateValue(Value); + } + /// /// Throws an exception if the double value is NaN or Inf. /// @@ -142,17 +176,6 @@ namespace Avalonia.Controls.Primitives } } - /// - /// Validates the property. - /// - /// The value. - /// The coerced value. - private double ValidateMinimum(double value) - { - ValidateDouble(value, "Minimum"); - return value; - } - /// /// Validates/coerces the property. /// @@ -160,7 +183,6 @@ namespace Avalonia.Controls.Primitives /// The coerced value. private double ValidateMaximum(double value) { - ValidateDouble(value, "Maximum"); return Math.Max(value, Minimum); } @@ -171,7 +193,6 @@ namespace Avalonia.Controls.Primitives /// The coerced value. private double ValidateValue(double value) { - ValidateDouble(value, "Value"); return MathUtilities.Clamp(value, Minimum, Maximum); } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs index 0d88a1a9ff..d913e3e54f 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs @@ -23,6 +23,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Minimum = 100, Maximum = 50, }; + var root = new TestRoot(target); Assert.Equal(100, target.Minimum); Assert.Equal(100, target.Maximum); @@ -37,6 +38,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Maximum = 50, Value = 100, }; + var root = new TestRoot(target); Assert.Equal(0, target.Minimum); Assert.Equal(50, target.Maximum); @@ -52,6 +54,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Maximum = 100, Value = 50, }; + var root = new TestRoot(target); target.Minimum = 200; @@ -69,6 +72,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Maximum = 100, Value = 100, }; + var root = new TestRoot(target); target.Maximum = 50; From 8f318ef75be59ac5199f47e068de9b0f37939ef1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2019 16:13:08 +0200 Subject: [PATCH 30/43] Make some fields readonly. --- src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 269f662a62..a2f4793958 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -104,7 +104,7 @@ namespace Avalonia.Controls.Primitives private static readonly IList Empty = Array.Empty(); - private Selection _selection = new Selection(); + private readonly Selection _selection = new Selection(); private int _selectedIndex = -1; private object _selectedItem; private IList _selectedItems; @@ -1071,7 +1071,7 @@ namespace Avalonia.Controls.Primitives private class Selection : IEnumerable { - private List _list = new List(); + private readonly List _list = new List(); private HashSet _set = new HashSet(); public bool HasItems => _set.Count > 0; From bf767c2c7397f351f6e1d81b27375396f2fea356 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2019 16:47:51 +0200 Subject: [PATCH 31/43] Fix clearing selection when clicking on selected item. --- .../Primitives/SelectingItemsControl.cs | 22 ++--------------- .../SelectingItemsControlTests_Multiple.cs | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index a2f4793958..ab07b3ffa9 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -999,26 +999,6 @@ namespace Avalonia.Controls.Primitives SelectedItemProperty, oldItem, item); - - UpdateSelectedItems(() => - { - if (clear) - { - SelectedItems.Clear(); - - if (index != -1) - { - SelectedItems.Add(item); - } - } - else - { - if (added != -1) - { - SelectedItems.Add(added); - } - } - }); } if (removed != null && index != -1) @@ -1028,6 +1008,8 @@ namespace Avalonia.Controls.Primitives if (added != -1 || removed?.Count > 0) { + ResetSelectedItems(); + var e = new SelectionChangedEventArgs( SelectionChangedEvent, added != -1 ? new[] { ElementAt(Items, added) } : Array.Empty(), diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index d8de98f4f2..4167a8e490 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -957,6 +957,30 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(new[] { "Foo", "Qux", "Baz" }, target.SelectedItems); } + [Fact] + public void Left_Click_On_SelectedItem_Should_Clear_Existing_Selection() + { + var target = new ListBox + { + 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.Down((Interactive)target.Presenter.Panel.Children[0]); + + Assert.Equal(1, target.SelectedItems.Count); + Assert.Equal(new[] { "Foo", }, target.SelectedItems); + Assert.Equal(new[] { 0 }, SelectedContainers(target)); + } + private IEnumerable SelectedContainers(SelectingItemsControl target) { return target.Presenter.Panel.Children From f84f6068bc7fbd6e7d5d46a5cce1dcd6edbe86b8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2019 17:14:42 +0200 Subject: [PATCH 32/43] Added failing tests for #2660. --- .../SelectingItemsControlTests_Multiple.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 4167a8e490..c557ea88be 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -981,6 +981,51 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(new[] { 0 }, SelectedContainers(target)); } + [Fact] + public void Right_Click_On_SelectedItem_Should_Not_Clear_Existing_Selection() + { + var target = new ListBox + { + 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], MouseButton.Right); + + Assert.Equal(3, target.SelectedItems.Count); + } + + [Fact] + public void Right_Click_On_UnselectedItem_Should_Clear_Existing_Selection() + { + var target = new ListBox + { + 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: InputModifiers.Shift); + + Assert.Equal(2, target.SelectedItems.Count); + + _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right); + + Assert.Equal(1, target.SelectedItems.Count); + } + private IEnumerable SelectedContainers(SelectingItemsControl target) { return target.Presenter.Panel.Children From 2b11ca12dd87b5d128a14bdb25cace0f1ff1252f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2019 17:15:53 +0200 Subject: [PATCH 33/43] Don't clear selection on right click. Fixes #2660. --- src/Avalonia.Controls/ListBox.cs | 3 ++- .../Primitives/SelectingItemsControl.cs | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 3568183459..3150b6be91 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -128,7 +128,8 @@ namespace Avalonia.Controls e.Source, true, (e.InputModifiers & InputModifiers.Shift) != 0, - (e.InputModifiers & InputModifiers.Control) != 0); + (e.InputModifiers & InputModifiers.Control) != 0, + e.MouseButton == MouseButton.Right); } } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index ab07b3ffa9..91a9fa7e40 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -506,11 +506,13 @@ namespace Avalonia.Controls.Primitives /// Whether the item should be selected or unselected. /// Whether the range modifier is enabled (i.e. shift key). /// Whether the toggle modifier is enabled (i.e. ctrl key). + /// Whether the event is a right-click. protected void UpdateSelection( int index, bool select = true, bool rangeModifier = false, - bool toggleModifier = false) + bool toggleModifier = false, + bool rightButton = false) { if (index != -1) { @@ -580,7 +582,7 @@ namespace Avalonia.Controls.Primitives } else { - SelectedIndex = index; + UpdateSelectedItem(index, !(rightButton && _selection.Contains(index))); } if (Presenter?.Panel != null) @@ -605,17 +607,19 @@ namespace Avalonia.Controls.Primitives /// Whether the container should be selected or unselected. /// Whether the range modifier is enabled (i.e. shift key). /// Whether the toggle modifier is enabled (i.e. ctrl key). + /// Whether the event is a right-click. protected void UpdateSelection( IControl container, bool select = true, bool rangeModifier = false, - bool toggleModifier = false) + bool toggleModifier = false, + bool rightButton = false) { var index = ItemContainerGenerator?.IndexFromContainer(container) ?? -1; if (index != -1) { - UpdateSelection(index, select, rangeModifier, toggleModifier); + UpdateSelection(index, select, rangeModifier, toggleModifier, rightButton); } } @@ -627,6 +631,7 @@ namespace Avalonia.Controls.Primitives /// Whether the container should be selected or unselected. /// Whether the range modifier is enabled (i.e. shift key). /// Whether the toggle modifier is enabled (i.e. ctrl key). + /// Whether the event is a right-click. /// /// True if the event originated from a container that belongs to the control; otherwise /// false. @@ -635,13 +640,14 @@ namespace Avalonia.Controls.Primitives IInteractive eventSource, bool select = true, bool rangeModifier = false, - bool toggleModifier = false) + bool toggleModifier = false, + bool rightButton = false) { var container = GetContainerFromEventSource(eventSource); if (container != null) { - UpdateSelection(container, select, rangeModifier, toggleModifier); + UpdateSelection(container, select, rangeModifier, toggleModifier, rightButton); return true; } From ccf60266e8cfe73f98e249cffd3820209d8f0f38 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2019 17:16:27 +0200 Subject: [PATCH 34/43] Send clicks rather than mouse down messages. --- .../SelectingItemsControlTests_Multiple.cs | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index c557ea88be..a33d97779e 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -633,7 +633,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - _helper.Down((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Shift); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Shift); var panel = target.Presenter.Panel; @@ -653,15 +653,15 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - _helper.Down((Interactive)target.Presenter.Panel.Children[1]); - _helper.Down((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control); - _helper.Down((Interactive)target.Presenter.Panel.Children[3], modifiers: InputModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[1]); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: InputModifiers.Control); Assert.Equal(1, target.SelectedIndex); Assert.Equal("Bar", target.SelectedItem); Assert.Equal(new[] { "Bar", "Baz", "Qux" }, target.SelectedItems); - _helper.Down((Interactive)target.Presenter.Panel.Children[1], modifiers: InputModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: InputModifiers.Control); Assert.Equal(2, target.SelectedIndex); Assert.Equal("Baz", target.SelectedItem); @@ -680,13 +680,13 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - _helper.Down((Interactive)target.Presenter.Panel.Children[1]); - _helper.Down((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[1]); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control); Assert.Equal(1, target.SelectedIndex); Assert.Equal("Bar", target.SelectedItem); - _helper.Down((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control); Assert.Equal(1, target.SelectedIndex); Assert.Equal("Bar", target.SelectedItem); @@ -704,8 +704,8 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - _helper.Down((Interactive)target.Presenter.Panel.Children[3]); - _helper.Down((Interactive)target.Presenter.Panel.Children[4], modifiers: InputModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[3]); + _helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: InputModifiers.Control); var panel = target.Presenter.Panel; @@ -725,8 +725,8 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - _helper.Down((Interactive)target.Presenter.Panel.Children[3]); - _helper.Down((Interactive)target.Presenter.Panel.Children[5], modifiers: InputModifiers.Shift); + _helper.Click((Interactive)target.Presenter.Panel.Children[3]); + _helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: InputModifiers.Shift); var panel = target.Presenter.Panel; @@ -746,8 +746,8 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - _helper.Down((Interactive)target.Presenter.Panel.Children[0]); - _helper.Down((Interactive)target.Presenter.Panel.Children[5], modifiers: InputModifiers.Shift); + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + _helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: InputModifiers.Shift); var panel = target.Presenter.Panel; @@ -767,19 +767,19 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - _helper.Down((Interactive)target.Presenter.Panel.Children[0]); + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); Assert.Equal(new[] { "Foo" }, target.SelectedItems); - _helper.Down((Interactive)target.Presenter.Panel.Children[4], modifiers: InputModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: InputModifiers.Control); Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); - _helper.Down((Interactive)target.Presenter.Panel.Children[3], modifiers: InputModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: InputModifiers.Control); Assert.Equal(new[] { "Foo", "Bar", "Foo" }, target.SelectedItems); - _helper.Down((Interactive)target.Presenter.Panel.Children[1], modifiers: InputModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: InputModifiers.Control); Assert.Equal(new[] { "Foo", "Bar", "Foo", "Bar" }, target.SelectedItems); } @@ -974,7 +974,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(3, target.SelectedItems.Count); - _helper.Down((Interactive)target.Presenter.Panel.Children[0]); + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); Assert.Equal(1, target.SelectedItems.Count); Assert.Equal(new[] { "Foo", }, target.SelectedItems); From 336086a588222d63e2e7a08266d113c10f77c883 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2019 17:51:06 +0200 Subject: [PATCH 35/43] Make ItemCount a direct property. Fixes #2658 --- src/Avalonia.Controls/ItemsControl.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 3dfeae52a4..a292ff7d0a 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -36,6 +36,12 @@ namespace Avalonia.Controls public static readonly DirectProperty ItemsProperty = AvaloniaProperty.RegisterDirect(nameof(Items), o => o.Items, (o, v) => o.Items = v); + /// + /// Defines the property. + /// + public static readonly DirectProperty ItemCountProperty = + AvaloniaProperty.RegisterDirect(nameof(ItemCount), o => o.ItemCount); + /// /// Defines the property. /// @@ -55,6 +61,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(MemberSelector)); private IEnumerable _items = new AvaloniaList(); + private int _itemCount; private IItemContainerGenerator _itemContainerGenerator; private IDisposable _itemsCollectionChangedSubscription; @@ -110,10 +117,13 @@ namespace Avalonia.Controls set { SetAndRaise(ItemsProperty, ref _items, value); } } + /// + /// Gets the number of items in . + /// public int ItemCount { - get; - private set; + get => _itemCount; + private set => SetAndRaise(ItemCountProperty, ref _itemCount, value); } /// From 1e115b89c8e980c9288bfcd8d0375a5fbce00c98 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 25 Jun 2019 19:55:09 +0300 Subject: [PATCH 36/43] Fixed Start() + obsoleted Start() --- src/Avalonia.Controls/AppBuilderBase.cs | 24 +++++++++++++------ .../DesktopApplicationExtensions.cs | 2 +- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 0f0b55e9e8..59ff35be76 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -4,6 +4,7 @@ using System; using System.Reflection; using System.Linq; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform; namespace Avalonia.Controls @@ -106,19 +107,28 @@ namespace Avalonia.Controls public void Start(Func dataContextProvider = null) where TMainWindow : Window, new() { - var window = new TMainWindow(); - if (dataContextProvider != null) - window.DataContext = dataContextProvider(); - Instance.Run(window); + AfterSetup(builder => + { + var window = new TMainWindow(); + if (dataContextProvider != null) + window.DataContext = dataContextProvider(); + ((IClassicDesktopStyleApplicationLifetime)builder.Instance.ApplicationLifetime) + .MainWindow = window; + }); + + // Copy-pasted because we can't call extension methods due to generic constraints + var lifetime = new ClassicDesktopStyleApplicationLifetime(Instance) {ShutdownMode = ShutdownMode.OnMainWindowClose}; + Instance.ApplicationLifetime = lifetime; + SetupWithoutStarting(); + lifetime.Start(Array.Empty()); } public delegate void AppMainDelegate(Application app, string[] args); - [Obsolete("Use either lifetimes or AppMain overload. See see https://github.com/AvaloniaUI/Avalonia/wiki/Application-lifetimes for details")] + [Obsolete("Use either lifetimes or AppMain overload. See see https://github.com/AvaloniaUI/Avalonia/wiki/Application-lifetimes for details", true)] public void Start() { - Setup(); - Instance.Run(); + throw new NotSupportedException(); } public void Start(AppMainDelegate main, string[] args) diff --git a/src/Avalonia.Controls/DesktopApplicationExtensions.cs b/src/Avalonia.Controls/DesktopApplicationExtensions.cs index 9b81bc94d1..ff6705cdc0 100644 --- a/src/Avalonia.Controls/DesktopApplicationExtensions.cs +++ b/src/Avalonia.Controls/DesktopApplicationExtensions.cs @@ -8,7 +8,7 @@ namespace Avalonia.Controls { public static class DesktopApplicationExtensions { - [Obsolete("Running application without a cancellation token and a lifetime is no longer supported, see https://github.com/AvaloniaUI/Avalonia/wiki/Application-lifetimes for details")] + [Obsolete("Running application without a cancellation token and a lifetime is no longer supported, see https://github.com/AvaloniaUI/Avalonia/wiki/Application-lifetimes for details", true)] public static void Run(this Application app) => throw new NotSupportedException(); /// From 5adf7c3a4cda4f598dd5ab93e04b7bb8a5f7bb91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Wed, 26 Jun 2019 17:48:03 +0100 Subject: [PATCH 37/43] Moved test to separate method. --- .../Data/BindingTests.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs index 9813c1f222..e412657711 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs @@ -24,6 +24,27 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data var window = (Window)loader.Load(xaml); var textBlock = window.FindControl("textBlock"); + window.DataContext = "foo"; + window.ApplyTemplate(); + + Assert.Equal("foo", textBlock.Text); + } + } + + [Fact] + public void Binding_To_DoNothing_Works() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + window.ApplyTemplate(); window.DataContext = "foo"; From fdd5d7afec4151862327a0b225359fd15291cf08 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Jun 2019 19:40:12 +0200 Subject: [PATCH 38/43] Added failing (skipped) test for #2592. --- .../Xaml/BindingTests.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs index 14abebcdb5..3930608515 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs @@ -331,6 +331,35 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [Fact(Skip="Issue #2592")] + public void MultiBinding_To_TextBlock_Text_With_StringConverter_Works() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + textBlock.DataContext = new WindowViewModel(); + window.ApplyTemplate(); + + Assert.Equal("Hello World!", textBlock.Text); + } + } + [Fact] public void Binding_OneWayToSource_Works() { @@ -356,6 +385,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml private class WindowViewModel { public bool ShowInTaskbar { get; set; } + public string Greeting1 { get; set; } = "Hello"; + public string Greeting2 { get; set; } = "World"; } } } From 9c56a4ba8412cca250a5574a717be54164cd9351 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 28 Jun 2019 15:21:05 +0300 Subject: [PATCH 39/43] [X11] Fixed NumLock --- src/Avalonia.X11/X11KeyTransform.cs | 10 +++------- src/Avalonia.X11/X11Window.cs | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.X11/X11KeyTransform.cs b/src/Avalonia.X11/X11KeyTransform.cs index 26495111d1..c68cb04733 100644 --- a/src/Avalonia.X11/X11KeyTransform.cs +++ b/src/Avalonia.X11/X11KeyTransform.cs @@ -221,12 +221,8 @@ namespace Avalonia.X11 //{ X11Key.?, Key.DeadCharProcessed } }; - public static Key ConvertKey(IntPtr key) - { - var ikey = key.ToInt32(); - Key result; - return KeyDic.TryGetValue((X11Key)ikey, out result) ? result : Key.None; - } -} + public static Key ConvertKey(X11Key key) + => KeyDic.TryGetValue(key, out var result) ? result : Key.None; + } } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 01beebfff1..18c23aa31e 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -419,10 +419,21 @@ namespace Avalonia.X11 return; var buffer = stackalloc byte[40]; - var latinKeysym = XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, 0); + var index = ev.KeyEvent.state.HasFlag(XModifierMask.ShiftMask); + + // We need the latin key, since it's mainly used for hotkeys, we use a different API for text anyway + var key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 1 : 0).ToInt32(); + + // Manually switch the Shift index for the keypad, + // there should be a proper way to do this + if (ev.KeyEvent.state.HasFlag(XModifierMask.Mod2Mask) + && key > X11Key.Num_Lock && key <= X11Key.KP_9) + key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 0 : 1).ToInt32(); + + ScheduleInput(new RawKeyEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), ev.type == XEventName.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, - X11KeyTransform.ConvertKey(latinKeysym), TranslateModifiers(ev.KeyEvent.state)), ref ev); + X11KeyTransform.ConvertKey(key), TranslateModifiers(ev.KeyEvent.state)), ref ev); if (ev.type == XEventName.KeyPress) { From 009f39ae743c72da1c058d2a31251b6b12c47379 Mon Sep 17 00:00:00 2001 From: artyom Date: Sun, 30 Jun 2019 19:43:19 +0300 Subject: [PATCH 40/43] Update AutoSuspendHelper to use IControlledApplicationLifetime --- src/Avalonia.ReactiveUI/AutoSuspendHelper.cs | 65 +++++++++++++++---- .../AutoSuspendHelperTest.cs | 14 ++-- 2 files changed, 64 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs b/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs index e39ecb4a0f..a4f14a4138 100644 --- a/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs +++ b/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs @@ -11,6 +11,8 @@ using System.Reactive.Linq; using System.Reactive; using ReactiveUI; using System; +using Avalonia.Controls.ApplicationLifetimes; +using Splat; namespace Avalonia.ReactiveUI { @@ -19,26 +21,67 @@ namespace Avalonia.ReactiveUI /// Avalonia applications. Call its constructor in your app's composition root, /// before calling the RxApp.SuspensionHost.SetupDefaultSuspendResume method. /// - public sealed class AutoSuspendHelper + public sealed class AutoSuspendHelper : IEnableLogger, IDisposable { - public AutoSuspendHelper(Application app) + private readonly Subject _shouldPersistState = new Subject(); + private readonly Subject _isLaunchingNew = new Subject(); + + /// + /// Initializes a new instance of the class. + /// + /// Pass in the Application.ApplicationLifetime property. + public AutoSuspendHelper(IApplicationLifetime lifetime) { RxApp.SuspensionHost.IsResuming = Observable.Never(); - RxApp.SuspensionHost.IsLaunchingNew = Observable.Return(Unit.Default); + RxApp.SuspensionHost.IsLaunchingNew = _isLaunchingNew; - var exiting = new Subject(); - app.Exit += (o, e) => + if (lifetime is IControlledApplicationLifetime controlled) { - // This is required to prevent the app from shutting down too early. - var manual = new ManualResetEvent(false); - exiting.OnNext(Disposable.Create(() => manual.Set())); - manual.WaitOne(); - }; - RxApp.SuspensionHost.ShouldPersistState = exiting; + this.Log().Debug("Using IControlledApplicationLifetime events to handle app exit."); + controlled.Exit += (sender, args) => OnControlledApplicationLifetimeExit(); + RxApp.SuspensionHost.ShouldPersistState = _shouldPersistState; + } + else if (lifetime != null) + { + var type = lifetime.GetType().FullName; + var message = $"Don't know how to detect app exit event for {type}."; + throw new NotSupportedException(message); + } + else + { + var message = "ApplicationLifetime is null. " + + "Ensure you are initializing AutoSuspendHelper " + + "when Avalonia application initialization is completed."; + throw new ArgumentNullException(message); + } var errored = new Subject(); AppDomain.CurrentDomain.UnhandledException += (o, e) => errored.OnNext(Unit.Default); RxApp.SuspensionHost.ShouldInvalidateState = errored; } + + /// + /// Call this method in your App.OnFrameworkInitializationCompleted method. + /// + public void OnFrameworkInitializationCompleted() => _isLaunchingNew.OnNext(Unit.Default); + + /// + /// Disposes internally stored observers. + /// + public void Dispose() + { + _shouldPersistState.Dispose(); + _isLaunchingNew.Dispose(); + } + + private void OnControlledApplicationLifetimeExit() + { + this.Log().Debug("Received IControlledApplicationLifetime exit event."); + var manual = new ManualResetEvent(false); + _shouldPersistState.OnNext(Disposable.Create(() => manual.Set())); + + manual.WaitOne(); + this.Log().Debug("Completed actions on IControlledApplicationLifetime exit event."); + } } } diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs index a3464878e6..d370454656 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs @@ -21,6 +21,7 @@ using Avalonia.ReactiveUI; using System.Reactive.Subjects; using System.Reactive.Linq; using System.Collections.Generic; +using System.Threading; namespace Avalonia.ReactiveUI.UnitTests { @@ -33,9 +34,10 @@ namespace Avalonia.ReactiveUI.UnitTests { var isLaunchingReceived = false; var application = AvaloniaLocator.Current.GetService(); - var suspension = new AutoSuspendHelper(application); + var suspension = new AutoSuspendHelper(application.ApplicationLifetime); RxApp.SuspensionHost.IsLaunchingNew.Subscribe(_ => isLaunchingReceived = true); + suspension.OnFrameworkInitializationCompleted(); Assert.True(isLaunchingReceived); } } @@ -47,12 +49,16 @@ namespace Avalonia.ReactiveUI.UnitTests { var shouldPersistReceived = false; var application = AvaloniaLocator.Current.GetService(); - var suspension = new AutoSuspendHelper(application); + var suspension = new AutoSuspendHelper(application.ApplicationLifetime); - RxApp.SuspensionHost.ShouldPersistState.Subscribe(_ => shouldPersistReceived = true); RxApp.SuspensionHost.SetupDefaultSuspendResume(new DummySuspensionDriver()); + RxApp.SuspensionHost.ShouldPersistState.Subscribe(_ => shouldPersistReceived = true); + suspension.OnFrameworkInitializationCompleted(); - application.Shutdown(); + var source = new CancellationTokenSource(); + source.CancelAfter(TimeSpan.FromMilliseconds(100)); + application.Run(source.Token); + Assert.True(shouldPersistReceived); } } From da467dd647432679b894be948b613f2f931fc3c7 Mon Sep 17 00:00:00 2001 From: artyom Date: Sun, 30 Jun 2019 21:00:18 +0300 Subject: [PATCH 41/43] Fix failing unit tests --- .../AutoSuspendHelperTest.cs | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs index d370454656..142eba41d5 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs @@ -4,40 +4,53 @@ using System; using System.Reactive.Concurrency; using System.Reactive.Disposables; +using System.ComponentModel; +using System.Threading.Tasks; +using System.Reactive; +using System.Reactive.Subjects; +using System.Reactive.Linq; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Threading; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls; using Avalonia.Rendering; using Avalonia.Platform; using Avalonia.UnitTests; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; using Avalonia; using ReactiveUI; using DynamicData; using Xunit; using Splat; -using Avalonia.Markup.Xaml; -using System.ComponentModel; -using System.Threading.Tasks; -using System.Reactive; -using Avalonia.ReactiveUI; -using System.Reactive.Subjects; -using System.Reactive.Linq; -using System.Collections.Generic; -using System.Threading; namespace Avalonia.ReactiveUI.UnitTests { public class AutoSuspendHelperTest { + [DataContract] + public class AppState + { + [DataMember] + public string Example { get; set; } + } + [Fact] public void AutoSuspendHelper_Should_Immediately_Fire_IsLaunchingNew() { using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + using (var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) { var isLaunchingReceived = false; var application = AvaloniaLocator.Current.GetService(); - var suspension = new AutoSuspendHelper(application.ApplicationLifetime); + application.ApplicationLifetime = lifetime; + // Initialize ReactiveUI Suspension as in real-world scenario. + var suspension = new AutoSuspendHelper(application.ApplicationLifetime); RxApp.SuspensionHost.IsLaunchingNew.Subscribe(_ => isLaunchingReceived = true); suspension.OnFrameworkInitializationCompleted(); + Assert.True(isLaunchingReceived); } } @@ -45,21 +58,23 @@ namespace Avalonia.ReactiveUI.UnitTests [Fact] public void ShouldPersistState_Should_Fire_On_App_Exit_When_SuspensionDriver_Is_Initialized() { - using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + using (var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) { var shouldPersistReceived = false; var application = AvaloniaLocator.Current.GetService(); - var suspension = new AutoSuspendHelper(application.ApplicationLifetime); + application.ApplicationLifetime = lifetime; - RxApp.SuspensionHost.SetupDefaultSuspendResume(new DummySuspensionDriver()); + // Initialize ReactiveUI Suspension as in real-world scenario. + var suspension = new AutoSuspendHelper(application.ApplicationLifetime); + RxApp.SuspensionHost.CreateNewAppState = () => new AppState { Example = "Foo" }; RxApp.SuspensionHost.ShouldPersistState.Subscribe(_ => shouldPersistReceived = true); + RxApp.SuspensionHost.SetupDefaultSuspendResume(new DummySuspensionDriver()); suspension.OnFrameworkInitializationCompleted(); - var source = new CancellationTokenSource(); - source.CancelAfter(TimeSpan.FromMilliseconds(100)); - application.Run(source.Token); - + lifetime.Shutdown(); Assert.True(shouldPersistReceived); + Assert.Equal("Foo", RxApp.SuspensionHost.GetAppState().Example); } } } From 6eed5187468b46f416837afc718bf1ff2d5bc450 Mon Sep 17 00:00:00 2001 From: artyom Date: Sun, 30 Jun 2019 21:08:12 +0300 Subject: [PATCH 42/43] Indicate exotic lifetimes are not supported --- .../AutoSuspendHelperTest.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs index 142eba41d5..876f37cc9e 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs @@ -36,6 +36,11 @@ namespace Avalonia.ReactiveUI.UnitTests public string Example { get; set; } } + public class ExoticApplicationLifetimeWithoutLifecycleEvents : IDisposable, IApplicationLifetime + { + public void Dispose() { } + } + [Fact] public void AutoSuspendHelper_Should_Immediately_Fire_IsLaunchingNew() { @@ -77,5 +82,17 @@ namespace Avalonia.ReactiveUI.UnitTests Assert.Equal("Foo", RxApp.SuspensionHost.GetAppState().Example); } } + + [Fact] + public void AutoSuspendHelper_Should_Throw_For_Not_Supported_Lifetimes() + { + using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + using (var lifetime = new ExoticApplicationLifetimeWithoutLifecycleEvents()) + { + var application = AvaloniaLocator.Current.GetService(); + application.ApplicationLifetime = lifetime; + Assert.Throws(() => new AutoSuspendHelper(application.ApplicationLifetime)); + } + } } } \ No newline at end of file From ae0feb21e07497bbd5811cbfe00b865a23941f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Sun, 30 Jun 2019 23:42:45 +0100 Subject: [PATCH 43/43] Fixed XML comment references. --- .../ControlledApplicationLifetimeExitEventArgs.cs | 2 +- .../ApplicationLifetimes/StartupEventArgs.cs | 2 +- src/Avalonia.Controls/ShutdownMode.cs | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs b/src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs index d4c3b27f7a..2963c019cc 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs @@ -6,7 +6,7 @@ using System; namespace Avalonia.Controls.ApplicationLifetimes { /// - /// Contains the arguments for the event. + /// Contains the arguments for the event. /// public class ControlledApplicationLifetimeExitEventArgs : EventArgs { diff --git a/src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs b/src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs index 4c08712707..423832793e 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs @@ -8,7 +8,7 @@ using System.Linq; namespace Avalonia.Controls.ApplicationLifetimes { /// - /// Contains the arguments for the event. + /// Contains the arguments for the event. /// public class ControlledApplicationLifetimeStartupEventArgs : EventArgs { diff --git a/src/Avalonia.Controls/ShutdownMode.cs b/src/Avalonia.Controls/ShutdownMode.cs index 46e27ff4e1..ee593c28a8 100644 --- a/src/Avalonia.Controls/ShutdownMode.cs +++ b/src/Avalonia.Controls/ShutdownMode.cs @@ -1,10 +1,12 @@ // 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 Avalonia.Controls.ApplicationLifetimes; + namespace Avalonia.Controls { /// - /// Describes the possible values for . + /// Describes the possible values for . /// public enum ShutdownMode {