Browse Source

Merge branch 'master' into experimental/itemsrepeater

pull/2603/head
Steven Kirk 7 years ago
committed by GitHub
parent
commit
cf070f2ee7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      src/Avalonia.Base/Data/BindingOperations.cs
  2. 46
      src/Avalonia.Base/Data/Converters/StringFormatMultiValueConverter.cs
  3. 31
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  4. 24
      src/Avalonia.Controls/AppBuilderBase.cs
  5. 2
      src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs
  6. 2
      src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs
  7. 33
      src/Avalonia.Controls/ColumnDefinition.cs
  8. 2
      src/Avalonia.Controls/DesktopApplicationExtensions.cs
  9. 173
      src/Avalonia.Controls/DockPanel.cs
  10. 14
      src/Avalonia.Controls/ItemsControl.cs
  11. 13
      src/Avalonia.Controls/ListBox.cs
  12. 65
      src/Avalonia.Controls/Primitives/RangeBase.cs
  13. 644
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  14. 41
      src/Avalonia.Controls/RowDefinition.cs
  15. 4
      src/Avalonia.Controls/ShutdownMode.cs
  16. 26
      src/Avalonia.Controls/TreeView.cs
  17. 10
      src/Avalonia.Controls/WrapPanel.cs
  18. 87
      src/Avalonia.ReactiveUI/AutoSuspendHelper.cs
  19. 10
      src/Avalonia.Styling/StyledElement.cs
  20. 10
      src/Avalonia.X11/X11KeyTransform.cs
  21. 15
      src/Avalonia.X11/X11Window.cs
  22. 31
      src/Markup/Avalonia.Markup/Data/MultiBinding.cs
  23. 37
      tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs
  24. 115
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  25. 523
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
  26. 40
      tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests_Converters.cs
  27. 71
      tests/Avalonia.Markup.Xaml.UnitTests/Converters/ValueConverterTests.cs
  28. 39
      tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs
  29. 19
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
  30. 31
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
  31. 98
      tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs
  32. 4
      tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs

2
src/Avalonia.Base/Data/BindingOperations.cs

@ -10,6 +10,8 @@ namespace Avalonia.Data
{ {
public static class BindingOperations public static class BindingOperations
{ {
public static readonly object DoNothing = new object();
/// <summary> /// <summary>
/// Applies an <see cref="InstancedBinding"/> a property on an <see cref="IAvaloniaObject"/>. /// Applies an <see cref="InstancedBinding"/> a property on an <see cref="IAvaloniaObject"/>.
/// </summary> /// </summary>

46
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
{
/// <summary>
/// A multi-value converter which calls <see cref="string.Format(string, object)"/>
/// </summary>
public class StringFormatMultiValueConverter : IMultiValueConverter
{
/// <summary>
/// Initializes a new instance of the <see cref="StringFormatMultiValueConverter"/> class.
/// </summary>
/// <param name="format">The format string.</param>
/// <param name="inner">
/// An optional inner converter to be called before the format takes place.
/// </param>
public StringFormatMultiValueConverter(string format, IMultiValueConverter inner)
{
Contract.Requires<ArgumentNullException>(format != null);
Format = format;
Inner = inner;
}
/// <summary>
/// Gets an inner value converter which will be called before the string format takes place.
/// </summary>
public IMultiValueConverter Inner { get; }
/// <summary>
/// Gets the format string.
/// </summary>
public string Format { get; }
/// <inheritdoc/>
public object Convert(IList<object> 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));
}
}
}

31
src/Avalonia.Base/Data/Core/BindingExpression.cs

@ -114,6 +114,11 @@ namespace Avalonia.Data.Core
/// <inheritdoc/> /// <inheritdoc/>
public void OnNext(object value) public void OnNext(object value)
{ {
if (value == BindingOperations.DoNothing)
{
return;
}
using (_inner.Subscribe(_ => { })) using (_inner.Subscribe(_ => { }))
{ {
var type = _inner.ResultType; var type = _inner.ResultType;
@ -126,6 +131,11 @@ namespace Avalonia.Data.Core
ConverterParameter, ConverterParameter,
CultureInfo.CurrentCulture); CultureInfo.CurrentCulture);
if (converted == BindingOperations.DoNothing)
{
return;
}
if (converted == AvaloniaProperty.UnsetValue) if (converted == AvaloniaProperty.UnsetValue)
{ {
converted = TypeUtilities.Default(type); converted = TypeUtilities.Default(type);
@ -186,6 +196,11 @@ namespace Avalonia.Data.Core
/// <inheritdoc/> /// <inheritdoc/>
private object ConvertValue(object value) private object ConvertValue(object value)
{ {
if (value == BindingOperations.DoNothing)
{
return value;
}
var notification = value as BindingNotification; var notification = value as BindingNotification;
if (notification == null) if (notification == null)
@ -196,6 +211,11 @@ namespace Avalonia.Data.Core
ConverterParameter, ConverterParameter,
CultureInfo.CurrentCulture); CultureInfo.CurrentCulture);
if (converted == BindingOperations.DoNothing)
{
return converted;
}
notification = converted as BindingNotification; notification = converted as BindingNotification;
if (notification?.ErrorType == BindingErrorType.None) if (notification?.ErrorType == BindingErrorType.None)
@ -327,7 +347,18 @@ namespace Avalonia.Data.Core
public void OnNext(object value) public void OnNext(object value)
{ {
if (value == BindingOperations.DoNothing)
{
return;
}
var converted = _owner.ConvertValue(value); var converted = _owner.ConvertValue(value);
if (converted == BindingOperations.DoNothing)
{
return;
}
_owner._value = new WeakReference<object>(converted); _owner._value = new WeakReference<object>(converted);
_owner.PublishNext(converted); _owner.PublishNext(converted);
} }

24
src/Avalonia.Controls/AppBuilderBase.cs

@ -4,6 +4,7 @@
using System; using System;
using System.Reflection; using System.Reflection;
using System.Linq; using System.Linq;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform; using Avalonia.Platform;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -106,19 +107,28 @@ namespace Avalonia.Controls
public void Start<TMainWindow>(Func<object> dataContextProvider = null) public void Start<TMainWindow>(Func<object> dataContextProvider = null)
where TMainWindow : Window, new() where TMainWindow : Window, new()
{ {
var window = new TMainWindow(); AfterSetup(builder =>
if (dataContextProvider != null) {
window.DataContext = dataContextProvider(); var window = new TMainWindow();
Instance.Run(window); 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<string>());
} }
public delegate void AppMainDelegate(Application app, string[] args); 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() public void Start()
{ {
Setup(); throw new NotSupportedException();
Instance.Run();
} }
public void Start(AppMainDelegate main, string[] args) public void Start(AppMainDelegate main, string[] args)

2
src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs

@ -6,7 +6,7 @@ using System;
namespace Avalonia.Controls.ApplicationLifetimes namespace Avalonia.Controls.ApplicationLifetimes
{ {
/// <summary> /// <summary>
/// Contains the arguments for the <see cref="IClassicDesktopStyleApplicationLifetime.Exit"/> event. /// Contains the arguments for the <see cref="IControlledApplicationLifetime.Exit"/> event.
/// </summary> /// </summary>
public class ControlledApplicationLifetimeExitEventArgs : EventArgs public class ControlledApplicationLifetimeExitEventArgs : EventArgs
{ {

2
src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs

@ -8,7 +8,7 @@ using System.Linq;
namespace Avalonia.Controls.ApplicationLifetimes namespace Avalonia.Controls.ApplicationLifetimes
{ {
/// <summary> /// <summary>
/// Contains the arguments for the <see cref="IClassicDesktopStyleApplicationLifetime.Startup"/> event. /// Contains the arguments for the <see cref="IControlledApplicationLifetime.Startup"/> event.
/// </summary> /// </summary>
public class ControlledApplicationLifetimeStartupEventArgs : EventArgs public class ControlledApplicationLifetimeStartupEventArgs : EventArgs
{ {

33
src/Avalonia.Controls/ColumnDefinition.cs

@ -62,8 +62,15 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public double MaxWidth public double MaxWidth
{ {
get { return GetValue(MaxWidthProperty); } get
set { SetValue(MaxWidthProperty, value); } {
return GetValue(MaxWidthProperty);
}
set
{
Parent?.InvalidateMeasure();
SetValue(MaxWidthProperty, value);
}
} }
/// <summary> /// <summary>
@ -71,8 +78,15 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public double MinWidth public double MinWidth
{ {
get { return GetValue(MinWidthProperty); } get
set { SetValue(MinWidthProperty, value); } {
return GetValue(MinWidthProperty);
}
set
{
Parent?.InvalidateMeasure();
SetValue(MinWidthProperty, value);
}
} }
/// <summary> /// <summary>
@ -80,8 +94,15 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public GridLength Width public GridLength Width
{ {
get { return GetValue(WidthProperty); } get
set { SetValue(WidthProperty, value); } {
return GetValue(WidthProperty);
}
set
{
Parent?.InvalidateMeasure();
SetValue(WidthProperty, value);
}
} }
internal override GridLength UserSizeValueCache => this.Width; internal override GridLength UserSizeValueCache => this.Width;

2
src/Avalonia.Controls/DesktopApplicationExtensions.cs

@ -8,7 +8,7 @@ namespace Avalonia.Controls
{ {
public static class DesktopApplicationExtensions 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(); public static void Run(this Application app) => throw new NotSupportedException();
/// <summary> /// <summary>

173
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 namespace Avalonia.Controls
{ {
using System;
/// <summary> /// <summary>
/// Defines the available docking modes for a control in a <see cref="DockPanel"/>. /// Defines the available docking modes for a control in a <see cref="DockPanel"/>.
/// </summary> /// </summary>
@ -70,107 +75,137 @@ namespace Avalonia.Controls
set { SetValue(LastChildFillProperty, value); } set { SetValue(LastChildFillProperty, value); }
} }
/// <inheritdoc/> /// <summary>
/// Updates DesiredSize of the DockPanel. Called by parent Control. This is the first pass of layout.
/// </summary>
/// <remarks>
/// Children are measured based on their sizing properties and <see cref="Dock" />.
/// 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.
/// </remarks>
/// <param name="constraint">Constraint size is an "upper limit" that the return value should not exceed.</param>
/// <returns>The Panel's desired size.</returns>
protected override Size MeasureOverride(Size constraint) protected override Size MeasureOverride(Size constraint)
{ {
double usedWidth = 0.0; var children = Children;
double usedHeight = 0.0;
double maximumWidth = 0.0; double parentWidth = 0; // Our current required width due to children thus far.
double maximumHeight = 0.0; 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.
// Measure each of the Children for (int i = 0, count = children.Count; i < count; ++i)
foreach (Control element in Children)
{ {
// Get the child's desired size var child = children[i];
Size remainingSize = new Size( Size childConstraint; // Contains the suggested input constraint for this child.
Math.Max(0.0, constraint.Width - usedWidth), Size childDesiredSize; // Contains the return size from child measure.
Math.Max(0.0, constraint.Height - usedHeight));
element.Measure(remainingSize); if (child == null)
Size desiredSize = element.DesiredSize; { continue; }
// Decrease the remaining space for the rest of the children // Child constraint is the remaining size; this is total size minus size consumed by previous children.
switch (GetDock(element)) 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.Left:
case Dock.Right: case Dock.Right:
maximumHeight = Math.Max(maximumHeight, usedHeight + desiredSize.Height); parentHeight = Math.Max(parentHeight, accumulatedHeight + childDesiredSize.Height);
usedWidth += desiredSize.Width; accumulatedWidth += childDesiredSize.Width;
break; break;
case Dock.Top: case Dock.Top:
case Dock.Bottom: case Dock.Bottom:
maximumWidth = Math.Max(maximumWidth, usedWidth + desiredSize.Width); parentWidth = Math.Max(parentWidth, accumulatedWidth + childDesiredSize.Width);
usedHeight += desiredSize.Height; accumulatedHeight += childDesiredSize.Height;
break; break;
} }
} }
maximumWidth = Math.Max(maximumWidth, usedWidth); // Make sure the final accumulated size is reflected in parentSize.
maximumHeight = Math.Max(maximumHeight, usedHeight); parentWidth = Math.Max(parentWidth, accumulatedWidth);
return new Size(maximumWidth, maximumHeight); parentHeight = Math.Max(parentHeight, accumulatedHeight);
return (new Size(parentWidth, parentHeight));
} }
/// <inheritdoc/> /// <summary>
/// DockPanel computes a position and final size for each of its children based upon their
/// <see cref="Dock" /> enum and sizing properties.
/// </summary>
/// <param name="arrangeSize">Size that DockPanel will assume to position children.</param>
protected override Size ArrangeOverride(Size arrangeSize) 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; var children = Children;
int dockedCount = children.Count - (LastChildFill ? 1 : 0); int totalChildrenCount = children.Count;
int index = 0; int nonFillChildrenCount = totalChildrenCount - (LastChildFill ? 1 : 0);
double accumulatedLeft = 0;
double accumulatedTop = 0;
double accumulatedRight = 0;
double accumulatedBottom = 0;
foreach (Control element in children) for (int i = 0; i < totalChildrenCount; ++i)
{ {
// Determine the remaining space left to arrange the element var child = children[i];
Rect remainingRect = new Rect( if (child == null)
left, { continue; }
top,
Math.Max(0.0, arrangeSize.Width - left - right), Size childDesiredSize = child.DesiredSize;
Math.Max(0.0, arrangeSize.Height - top - bottom)); Rect rcChild = new Rect(
accumulatedLeft,
// Trim the remaining Rect to the docked size of the element accumulatedTop,
// (unless the element should fill the remaining space because Math.Max(0.0, arrangeSize.Width - (accumulatedLeft + accumulatedRight)),
// of LastChildFill) Math.Max(0.0, arrangeSize.Height - (accumulatedTop + accumulatedBottom)));
if (index < dockedCount)
if (i < nonFillChildrenCount)
{ {
Size desiredSize = element.DesiredSize; switch (DockPanel.GetDock((Control)child))
switch (GetDock(element))
{ {
case Dock.Left: case Dock.Left:
left += desiredSize.Width; accumulatedLeft += childDesiredSize.Width;
remainingRect = remainingRect.WithWidth(desiredSize.Width); rcChild = rcChild.WithWidth(childDesiredSize.Width);
break;
case Dock.Top:
top += desiredSize.Height;
remainingRect = remainingRect.WithHeight(desiredSize.Height);
break; break;
case Dock.Right: case Dock.Right:
right += desiredSize.Width; accumulatedRight += childDesiredSize.Width;
remainingRect = new Rect( rcChild = rcChild.WithX(Math.Max(0.0, arrangeSize.Width - accumulatedRight));
Math.Max(0.0, arrangeSize.Width - right), rcChild = rcChild.WithWidth(childDesiredSize.Width);
remainingRect.Y,
desiredSize.Width,
remainingRect.Height);
break; break;
case Dock.Top:
accumulatedTop += childDesiredSize.Height;
rcChild = rcChild.WithHeight(childDesiredSize.Height);
break;
case Dock.Bottom: case Dock.Bottom:
bottom += desiredSize.Height; accumulatedBottom += childDesiredSize.Height;
remainingRect = new Rect( rcChild = rcChild.WithY(Math.Max(0.0, arrangeSize.Height - accumulatedBottom));
remainingRect.X, rcChild = rcChild.WithHeight(childDesiredSize.Height);
Math.Max(0.0, arrangeSize.Height - bottom),
remainingRect.Width,
desiredSize.Height);
break; break;
} }
} }
element.Arrange(remainingRect); child.Arrange(rcChild);
index++;
} }
return arrangeSize; return (arrangeSize);
} }
} }
} }

14
src/Avalonia.Controls/ItemsControl.cs

@ -36,6 +36,12 @@ namespace Avalonia.Controls
public static readonly DirectProperty<ItemsControl, IEnumerable> ItemsProperty = public static readonly DirectProperty<ItemsControl, IEnumerable> ItemsProperty =
AvaloniaProperty.RegisterDirect<ItemsControl, IEnumerable>(nameof(Items), o => o.Items, (o, v) => o.Items = v); AvaloniaProperty.RegisterDirect<ItemsControl, IEnumerable>(nameof(Items), o => o.Items, (o, v) => o.Items = v);
/// <summary>
/// Defines the <see cref="ItemCount"/> property.
/// </summary>
public static readonly DirectProperty<ItemsControl, int> ItemCountProperty =
AvaloniaProperty.RegisterDirect<ItemsControl, int>(nameof(ItemCount), o => o.ItemCount);
/// <summary> /// <summary>
/// Defines the <see cref="ItemsPanel"/> property. /// Defines the <see cref="ItemsPanel"/> property.
/// </summary> /// </summary>
@ -55,6 +61,7 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<ItemsControl, IMemberSelector>(nameof(MemberSelector)); AvaloniaProperty.Register<ItemsControl, IMemberSelector>(nameof(MemberSelector));
private IEnumerable _items = new AvaloniaList<object>(); private IEnumerable _items = new AvaloniaList<object>();
private int _itemCount;
private IItemContainerGenerator _itemContainerGenerator; private IItemContainerGenerator _itemContainerGenerator;
private IDisposable _itemsCollectionChangedSubscription; private IDisposable _itemsCollectionChangedSubscription;
@ -110,10 +117,13 @@ namespace Avalonia.Controls
set { SetAndRaise(ItemsProperty, ref _items, value); } set { SetAndRaise(ItemsProperty, ref _items, value); }
} }
/// <summary>
/// Gets the number of items in <see cref="Items"/>.
/// </summary>
public int ItemCount public int ItemCount
{ {
get; get => _itemCount;
private set; private set => SetAndRaise(ItemCountProperty, ref _itemCount, value);
} }
/// <summary> /// <summary>

13
src/Avalonia.Controls/ListBox.cs

@ -84,6 +84,16 @@ namespace Avalonia.Controls
set { SetValue(VirtualizationModeProperty, value); } set { SetValue(VirtualizationModeProperty, value); }
} }
/// <summary>
/// Selects all items in the <see cref="ListBox"/>.
/// </summary>
public new void SelectAll() => base.SelectAll();
/// <summary>
/// Deselects all items in the <see cref="ListBox"/>.
/// </summary>
public new void UnselectAll() => base.UnselectAll();
/// <inheritdoc/> /// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator() protected override IItemContainerGenerator CreateItemContainerGenerator()
{ {
@ -118,7 +128,8 @@ namespace Avalonia.Controls
e.Source, e.Source,
true, true,
(e.InputModifiers & InputModifiers.Shift) != 0, (e.InputModifiers & InputModifiers.Shift) != 0,
(e.InputModifiers & InputModifiers.Control) != 0); (e.InputModifiers & InputModifiers.Control) != 0,
e.MouseButton == MouseButton.Right);
} }
} }

65
src/Avalonia.Controls/Primitives/RangeBase.cs

@ -75,10 +75,18 @@ namespace Avalonia.Controls.Primitives
set set
{ {
value = ValidateMinimum(value); ValidateDouble(value, "Minimum");
SetAndRaise(MinimumProperty, ref _minimum, value);
Maximum = ValidateMaximum(Maximum); if (IsInitialized)
Value = ValidateValue(Value); {
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 set
{ {
value = ValidateMaximum(value); ValidateDouble(value, "Maximum");
SetAndRaise(MaximumProperty, ref _maximum, value);
Value = ValidateValue(Value); 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 set
{ {
value = ValidateValue(value); ValidateDouble(value, "Value");
SetAndRaise(ValueProperty, ref _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); set => SetValue(LargeChangeProperty, value);
} }
protected override void OnInitialized()
{
base.OnInitialized();
Maximum = ValidateMaximum(Maximum);
Value = ValidateValue(Value);
}
/// <summary> /// <summary>
/// Throws an exception if the double value is NaN or Inf. /// Throws an exception if the double value is NaN or Inf.
/// </summary> /// </summary>
@ -142,17 +176,6 @@ namespace Avalonia.Controls.Primitives
} }
} }
/// <summary>
/// Validates the <see cref="Minimum"/> property.
/// </summary>
/// <param name="value">The value.</param>
/// <returns>The coerced value.</returns>
private double ValidateMinimum(double value)
{
ValidateDouble(value, "Minimum");
return value;
}
/// <summary> /// <summary>
/// Validates/coerces the <see cref="Maximum"/> property. /// Validates/coerces the <see cref="Maximum"/> property.
/// </summary> /// </summary>
@ -160,7 +183,6 @@ namespace Avalonia.Controls.Primitives
/// <returns>The coerced value.</returns> /// <returns>The coerced value.</returns>
private double ValidateMaximum(double value) private double ValidateMaximum(double value)
{ {
ValidateDouble(value, "Maximum");
return Math.Max(value, Minimum); return Math.Max(value, Minimum);
} }
@ -171,7 +193,6 @@ namespace Avalonia.Controls.Primitives
/// <returns>The coerced value.</returns> /// <returns>The coerced value.</returns>
private double ValidateValue(double value) private double ValidateValue(double value)
{ {
ValidateDouble(value, "Value");
return MathUtilities.Clamp(value, Minimum, Maximum); return MathUtilities.Clamp(value, Minimum, Maximum);
} }
} }

644
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -12,6 +12,7 @@ using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Logging;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -103,6 +104,7 @@ namespace Avalonia.Controls.Primitives
private static readonly IList Empty = Array.Empty<object>(); private static readonly IList Empty = Array.Empty<object>();
private readonly Selection _selection = new Selection();
private int _selectedIndex = -1; private int _selectedIndex = -1;
private object _selectedItem; private object _selectedItem;
private IList _selectedItems; private IList _selectedItems;
@ -152,23 +154,8 @@ namespace Avalonia.Controls.Primitives
{ {
if (_updateCount == 0) if (_updateCount == 0)
{ {
SetAndRaise(SelectedIndexProperty, ref _selectedIndex, (int val, ref int backing, Action<Action> notifyWrapper) => var effective = (value >= 0 && value < ItemCount) ? value : -1;
{ UpdateSelectedItem(effective);
var old = backing;
var effective = (val >= 0 && val < Items?.Cast<object>().Count()) ? val : -1;
if (old != effective)
{
backing = effective;
notifyWrapper(() =>
RaisePropertyChanged(
SelectedIndexProperty,
old,
effective,
BindingPriority.LocalValue));
SelectedItem = ElementAt(Items, effective);
}
}, value);
} }
else else
{ {
@ -192,41 +179,7 @@ namespace Avalonia.Controls.Primitives
{ {
if (_updateCount == 0) if (_updateCount == 0)
{ {
SetAndRaise(SelectedItemProperty, ref _selectedItem, (object val, ref object backing, Action<Action> notifyWrapper) => UpdateSelectedItem(IndexOf(Items, value));
{
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);
} }
else else
{ {
@ -354,31 +307,23 @@ namespace Avalonia.Controls.Primitives
{ {
SelectedIndex = 0; SelectedIndex = 0;
} }
else
{
_selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count);
UpdateSelectedItem(_selection.First(), false);
}
break; break;
case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Replace: _selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count);
var selectedIndex = SelectedIndex; UpdateSelectedItem(_selection.First(), false);
ResetSelectedItems();
if (selectedIndex >= e.OldStartingIndex && break;
selectedIndex < e.OldStartingIndex + e.OldItems.Count)
{
if (!AlwaysSelected)
{
selectedIndex = SelectedIndex = -1;
}
else
{
LostSelection();
}
}
var items = Items?.Cast<object>(); case NotifyCollectionChangedAction.Replace:
if (selectedIndex >= items.Count()) UpdateSelectedItem(SelectedIndex, false);
{ ResetSelectedItems();
selectedIndex = SelectedIndex = items.Count() - 1;
}
break; break;
case NotifyCollectionChangedAction.Move: case NotifyCollectionChangedAction.Move:
@ -439,11 +384,7 @@ namespace Avalonia.Controls.Primitives
{ {
if (i.ContainerControl != null && i.Item != null) if (i.ContainerControl != null && i.Item != null)
{ {
var ms = MemberSelector; bool selected = _selection.Contains(i.Index);
bool selected = ms == null ?
SelectedItems.Contains(i.Item) :
SelectedItems.OfType<object>().Any(v => Equals(ms.Select(v), i.Item));
MarkContainerSelected(i.ContainerControl, selected); MarkContainerSelected(i.ContainerControl, selected);
} }
} }
@ -476,9 +417,12 @@ namespace Avalonia.Controls.Primitives
var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>(); var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e)); bool Match(List<KeyGesture> 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<object>()); SelectAll();
e.Handled = true; e.Handled = true;
} }
} }
@ -520,6 +464,41 @@ namespace Avalonia.Controls.Primitives
return false; return false;
} }
/// <summary>
/// Selects all items in the control.
/// </summary>
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);
}
UpdateSelectedItem(0, false);
foreach (var container in ItemContainerGenerator.Containers)
{
MarkItemSelected(container.Index, true);
}
ResetSelectedItems();
});
}
/// <summary>
/// Deselects all items in the control.
/// </summary>
protected void UnselectAll() => UpdateSelectedItem(-1);
/// <summary> /// <summary>
/// Updates the selection for an item based on user interaction. /// Updates the selection for an item based on user interaction.
/// </summary> /// </summary>
@ -527,51 +506,83 @@ namespace Avalonia.Controls.Primitives
/// <param name="select">Whether the item should be selected or unselected.</param> /// <param name="select">Whether the item should be selected or unselected.</param>
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param> /// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param> /// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
/// <param name="rightButton">Whether the event is a right-click.</param>
protected void UpdateSelection( protected void UpdateSelection(
int index, int index,
bool select = true, bool select = true,
bool rangeModifier = false, bool rangeModifier = false,
bool toggleModifier = false) bool toggleModifier = false,
bool rightButton = false)
{ {
if (index != -1) if (index != -1)
{ {
if (select) if (select)
{ {
var mode = SelectionMode; var mode = SelectionMode;
var toggle = toggleModifier || (mode & SelectionMode.Toggle) != 0;
var multi = (mode & SelectionMode.Multiple) != 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) if (range)
{ {
SelectedIndex = index; UpdateSelectedItems(() =>
} {
else if (multi && range) var start = SelectedIndex != -1 ? SelectedIndex : 0;
{ var step = start < index ? 1 : -1;
SynchronizeItems(
SelectedItems, _selection.Clear();
GetRange(Items, SelectedIndex, index));
for (var i = start; i != index; i += step)
{
_selection.Add(i);
}
_selection.Add(index);
var first = Math.Min(start, index);
var last = Math.Max(start, index);
foreach (var container in ItemContainerGenerator.Containers)
{
MarkItemSelected(
container.Index,
container.Index >= first && container.Index <= last);
}
ResetSelectedItems();
});
} }
else else if (multi && toggle)
{ {
var item = ElementAt(Items, index); UpdateSelectedItems(() =>
var i = SelectedItems.IndexOf(item);
if (i != -1 && (!AlwaysSelected || SelectedItems.Count > 1))
{
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 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
{
UpdateSelectedItem(index, !(rightButton && _selection.Contains(index)));
} }
if (Presenter?.Panel != null) if (Presenter?.Panel != null)
@ -596,17 +607,19 @@ namespace Avalonia.Controls.Primitives
/// <param name="select">Whether the container should be selected or unselected.</param> /// <param name="select">Whether the container should be selected or unselected.</param>
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param> /// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param> /// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
/// <param name="rightButton">Whether the event is a right-click.</param>
protected void UpdateSelection( protected void UpdateSelection(
IControl container, IControl container,
bool select = true, bool select = true,
bool rangeModifier = false, bool rangeModifier = false,
bool toggleModifier = false) bool toggleModifier = false,
bool rightButton = false)
{ {
var index = ItemContainerGenerator?.IndexFromContainer(container) ?? -1; var index = ItemContainerGenerator?.IndexFromContainer(container) ?? -1;
if (index != -1) if (index != -1)
{ {
UpdateSelection(index, select, rangeModifier, toggleModifier); UpdateSelection(index, select, rangeModifier, toggleModifier, rightButton);
} }
} }
@ -618,6 +631,7 @@ namespace Avalonia.Controls.Primitives
/// <param name="select">Whether the container should be selected or unselected.</param> /// <param name="select">Whether the container should be selected or unselected.</param>
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param> /// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param> /// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
/// <param name="rightButton">Whether the event is a right-click.</param>
/// <returns> /// <returns>
/// True if the event originated from a container that belongs to the control; otherwise /// True if the event originated from a container that belongs to the control; otherwise
/// false. /// false.
@ -626,51 +640,20 @@ namespace Avalonia.Controls.Primitives
IInteractive eventSource, IInteractive eventSource,
bool select = true, bool select = true,
bool rangeModifier = false, bool rangeModifier = false,
bool toggleModifier = false) bool toggleModifier = false,
bool rightButton = false)
{ {
var container = GetContainerFromEventSource(eventSource); var container = GetContainerFromEventSource(eventSource);
if (container != null) if (container != null)
{ {
UpdateSelection(container, select, rangeModifier, toggleModifier); UpdateSelection(container, select, rangeModifier, toggleModifier, rightButton);
return true; return true;
} }
return false; return false;
} }
/// <summary>
/// Makes a list of objects equal another.
/// </summary>
/// <param name="items">The items collection.</param>
/// <param name="desired">The desired items.</param>
internal static void SynchronizeItems(IList items, IEnumerable<object> desired)
{
var index = 0;
foreach (object item in desired)
{
int itemIndex = items.IndexOf(item);
if (itemIndex == -1)
{
items.Insert(index, item);
}
else if(itemIndex != index)
{
items.RemoveAt(itemIndex);
items.Insert(index, item);
}
++index;
}
while (index < items.Count)
{
items.RemoveAt(items.Count - 1);
}
}
/// <summary> /// <summary>
/// Gets a range of items from an IEnumerable. /// Gets a range of items from an IEnumerable.
/// </summary> /// </summary>
@ -678,17 +661,19 @@ namespace Avalonia.Controls.Primitives
/// <param name="first">The index of the first item.</param> /// <param name="first">The index of the first item.</param>
/// <param name="last">The index of the last item.</param> /// <param name="last">The index of the last item.</param>
/// <returns>The items.</returns> /// <returns>The items.</returns>
private static IEnumerable<object> GetRange(IEnumerable items, int first, int last) private static List<object> GetRange(IEnumerable items, int first, int last)
{ {
var list = (items as IList) ?? items.Cast<object>().ToList(); var list = (items as IList) ?? items.Cast<object>().ToList();
int step = first > last ? -1 : 1; var step = first > last ? -1 : 1;
var result = new List<object>();
for (int i = first; i != last; i += step) 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;
} }
/// <summary> /// <summary>
@ -724,19 +709,14 @@ namespace Avalonia.Controls.Primitives
private void LostSelection() private void LostSelection()
{ {
var items = Items?.Cast<object>(); var items = Items?.Cast<object>();
var index = -1;
if (items != null && AlwaysSelected) if (items != null && AlwaysSelected)
{ {
var index = Math.Min(SelectedIndex, items.Count() - 1); index = Math.Min(SelectedIndex, items.Count() - 1);
if (index > -1)
{
SelectedItem = items.ElementAt(index);
return;
}
} }
SelectedIndex = -1; SelectedIndex = index;
} }
/// <summary> /// <summary>
@ -793,7 +773,7 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
/// <param name="item">The item.</param> /// <param name="item">The item.</param>
/// <param name="selected">Whether the item should be selected or deselected.</param> /// <param name="selected">Whether the item should be selected or deselected.</param>
private void MarkItemSelected(object item, bool selected) private int MarkItemSelected(object item, bool selected)
{ {
var index = IndexOf(Items, item); var index = IndexOf(Items, item);
@ -801,6 +781,21 @@ namespace Avalonia.Controls.Primitives
{ {
MarkItemSelected(index, selected); MarkItemSelected(index, selected);
} }
return index;
}
private void ResetSelectedItems()
{
UpdateSelectedItems(() =>
{
SelectedItems.Clear();
foreach (var i in _selection)
{
SelectedItems.Add(ElementAt(Items, i));
}
});
} }
/// <summary> /// <summary>
@ -810,95 +805,97 @@ namespace Avalonia.Controls.Primitives
/// <param name="e">The event args.</param> /// <param name="e">The event args.</param>
private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 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 added = null;
IList removed = null; IList removed = null;
switch (e.Action) switch (e.Action)
{ {
case NotifyCollectionChangedAction.Add: case NotifyCollectionChangedAction.Add:
SelectedItemsAdded(e.NewItems.Cast<object>().ToList());
if (AutoScrollToSelectedItem)
{ {
ScrollIntoView(e.NewItems[0]); Add(e.NewItems);
UpdateSelection();
added = e.NewItems;
} }
added = e.NewItems;
break; break;
case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Remove:
if (SelectedItems.Count == 0) if (SelectedItems.Count == 0)
{ {
if (!_syncingSelectedItems) SelectedIndex = -1;
{
SelectedIndex = -1;
}
} }
foreach (var item in e.OldItems) foreach (var item in e.OldItems)
{ {
MarkItemSelected(item, false); var index = MarkItemSelected(item, false);
_selection.Remove(index);
} }
removed = e.OldItems; removed = e.OldItems;
break; 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: case NotifyCollectionChangedAction.Reset:
if (generator != null)
{ {
removed = new List<object>(); removed = new List<object>();
added = new List<object>();
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)) MarkItemSelected(index, false);
{ removed.Add(item);
removed.Add(item.Item); _selection.Remove(index);
}
} }
} }
}
if (SelectedItems.Count > 0) Add(SelectedItems, added);
{ UpdateSelection();
_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);
}
added = e.NewItems;
removed = e.OldItems;
break; break;
} }
@ -912,34 +909,6 @@ namespace Avalonia.Controls.Primitives
} }
} }
/// <summary>
/// Called when items are added to the <see cref="SelectedItems"/> collection.
/// </summary>
/// <param name="items">The added items.</param>
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);
}
}
}
}
/// <summary> /// <summary>
/// Subscribes to the <see cref="SelectedItems"/> CollectionChanged event, if any. /// Subscribes to the <see cref="SelectedItems"/> CollectionChanged event, if any.
/// </summary> /// </summary>
@ -970,6 +939,112 @@ namespace Avalonia.Controls.Primitives
} }
} }
/// <summary>
/// Updates the selection due to a change to <see cref="SelectedIndex"/> or
/// <see cref="SelectedItem"/>.
/// </summary>
/// <param name="index">The new selected index.</param>
/// <param name="clear">Whether to clear existing selection.</param>
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<int> 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);
}
if (removed != null && index != -1)
{
removed.Remove(index);
}
if (added != -1 || removed?.Count > 0)
{
ResetSelectedItems();
var e = new SelectionChangedEventArgs(
SelectionChangedEvent,
added != -1 ? new[] { ElementAt(Items, added) } : Array.Empty<object>(),
removed?.Select(x => ElementAt(Items, x)).ToArray() ?? Array.Empty<object>());
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() private void UpdateFinished()
{ {
if (_updateSelectedIndex != int.MinValue) if (_updateSelectedIndex != int.MinValue)
@ -981,5 +1056,104 @@ namespace Avalonia.Controls.Primitives
SelectedItems = _updateSelectedItems; SelectedItems = _updateSelectedItems;
} }
} }
private class Selection : IEnumerable<int>
{
private readonly List<int> _list = new List<int>();
private HashSet<int> _set = new HashSet<int>();
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<int> Clear()
{
var result = _set;
_list.Clear();
_set = new HashSet<int>();
return result;
}
public void ItemsInserted(int index, int count)
{
_set = new HashSet<int>();
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<int>();
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;
public IEnumerator<int> GetEnumerator() => _set.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
} }
} }

41
src/Avalonia.Controls/RowDefinition.cs

@ -29,7 +29,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="RowDefinition"/> class. /// Initializes a new instance of the <see cref="RowDefinition"/> class.
/// </summary> /// </summary>
public RowDefinition() public RowDefinition()
{ {
} }
@ -38,7 +38,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
/// <param name="value">The height of the row.</param> /// <param name="value">The height of the row.</param>
/// <param name="type">The height unit of the column.</param> /// <param name="type">The height unit of the column.</param>
public RowDefinition(double value, GridUnitType type) public RowDefinition(double value, GridUnitType type)
{ {
Height = new GridLength(value, type); Height = new GridLength(value, type);
} }
@ -47,7 +47,7 @@ namespace Avalonia.Controls
/// Initializes a new instance of the <see cref="RowDefinition"/> class. /// Initializes a new instance of the <see cref="RowDefinition"/> class.
/// </summary> /// </summary>
/// <param name="height">The height of the column.</param> /// <param name="height">The height of the column.</param>
public RowDefinition(GridLength height) public RowDefinition(GridLength height)
{ {
Height = height; Height = height;
} }
@ -62,8 +62,15 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public double MaxHeight public double MaxHeight
{ {
get { return GetValue(MaxHeightProperty); } get
set { SetValue(MaxHeightProperty, value); } {
return GetValue(MaxHeightProperty);
}
set
{
Parent?.InvalidateMeasure();
SetValue(MaxHeightProperty, value);
}
} }
/// <summary> /// <summary>
@ -71,8 +78,15 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public double MinHeight public double MinHeight
{ {
get { return GetValue(MinHeightProperty); } get
set { SetValue(MinHeightProperty, value); } {
return GetValue(MinHeightProperty);
}
set
{
Parent?.InvalidateMeasure();
SetValue(MinHeightProperty, value);
}
} }
/// <summary> /// <summary>
@ -80,12 +94,19 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public GridLength Height public GridLength Height
{ {
get { return GetValue(HeightProperty); } get
set { SetValue(HeightProperty, value); } {
return GetValue(HeightProperty);
}
set
{
Parent?.InvalidateMeasure();
SetValue(HeightProperty, value);
}
} }
internal override GridLength UserSizeValueCache => this.Height; internal override GridLength UserSizeValueCache => this.Height;
internal override double UserMinSizeValueCache => this.MinHeight; internal override double UserMinSizeValueCache => this.MinHeight;
internal override double UserMaxSizeValueCache => this.MaxHeight; internal override double UserMaxSizeValueCache => this.MaxHeight;
} }
} }

4
src/Avalonia.Controls/ShutdownMode.cs

@ -1,10 +1,12 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Controls.ApplicationLifetimes;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// Describes the possible values for <see cref="Application.ShutdownMode"/>. /// Describes the possible values for <see cref="IClassicDesktopStyleApplicationLifetime.ShutdownMode"/>.
/// </summary> /// </summary>
public enum ShutdownMode public enum ShutdownMode
{ {

26
src/Avalonia.Controls/TreeView.cs

@ -409,7 +409,7 @@ namespace Avalonia.Controls
if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll)) if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll))
{ {
SelectingItemsControl.SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items); SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items);
e.Handled = true; e.Handled = true;
} }
} }
@ -521,7 +521,7 @@ namespace Avalonia.Controls
} }
else if (multi && range) else if (multi && range)
{ {
SelectingItemsControl.SynchronizeItems( SynchronizeItems(
SelectedItems, SelectedItems,
GetItemsInRange(selectedContainer as TreeViewItem, container as TreeViewItem)); GetItemsInRange(selectedContainer as TreeViewItem, container as TreeViewItem));
} }
@ -778,5 +778,27 @@ namespace Avalonia.Controls
container.Classes.Set(":selected", selected); container.Classes.Set(":selected", selected);
} }
} }
/// <summary>
/// Makes a list of objects equal another (though doesn't preserve order).
/// </summary>
/// <param name="items">The items collection.</param>
/// <param name="desired">The desired items.</param>
private static void SynchronizeItems(IList items, IEnumerable<object> desired)
{
var list = items.Cast<object>().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);
}
}
} }
} }

10
src/Avalonia.Controls/WrapPanel.cs

@ -1,9 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved. // This source file is adapted from the Windows Presentation Foundation project.
// Licensed under the MIT license. See licence.md file in the project root for full license information. // (https://github.com/dotnet/wpf/)
//
using System.Collections.Generic; // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System.Diagnostics;
using System.Linq;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Layout; using Avalonia.Layout;

87
src/Avalonia.ReactiveUI/AutoSuspendHelper.cs

@ -0,0 +1,87 @@
// 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;
using Avalonia.Controls.ApplicationLifetimes;
using Splat;
namespace Avalonia.ReactiveUI
{
/// <summary>
/// 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.
/// </summary>
public sealed class AutoSuspendHelper : IEnableLogger, IDisposable
{
private readonly Subject<IDisposable> _shouldPersistState = new Subject<IDisposable>();
private readonly Subject<Unit> _isLaunchingNew = new Subject<Unit>();
/// <summary>
/// Initializes a new instance of the <see cref="AutoSuspendHelper"/> class.
/// </summary>
/// <param name="lifetime">Pass in the Application.ApplicationLifetime property.</param>
public AutoSuspendHelper(IApplicationLifetime lifetime)
{
RxApp.SuspensionHost.IsResuming = Observable.Never<Unit>();
RxApp.SuspensionHost.IsLaunchingNew = _isLaunchingNew;
if (lifetime is IControlledApplicationLifetime controlled)
{
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<Unit>();
AppDomain.CurrentDomain.UnhandledException += (o, e) => errored.OnNext(Unit.Default);
RxApp.SuspensionHost.ShouldInvalidateState = errored;
}
/// <summary>
/// Call this method in your App.OnFrameworkInitializationCompleted method.
/// </summary>
public void OnFrameworkInitializationCompleted() => _isLaunchingNew.OnNext(Unit.Default);
/// <summary>
/// Disposes internally stored observers.
/// </summary>
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.");
}
}
}

10
src/Avalonia.Styling/StyledElement.cs

@ -392,6 +392,7 @@ namespace Avalonia
if (_initCount == 0 && !IsInitialized) if (_initCount == 0 && !IsInitialized)
{ {
IsInitialized = true; IsInitialized = true;
OnInitialized();
Initialized?.Invoke(this, EventArgs.Empty); Initialized?.Invoke(this, EventArgs.Empty);
} }
} }
@ -608,7 +609,14 @@ namespace Avalonia
protected virtual void OnDataContextEndUpdate() protected virtual void OnDataContextEndUpdate()
{ {
} }
/// <summary>
/// Called when the control finishes initialization.
/// </summary>
protected virtual void OnInitialized()
{
}
private static void DataContextNotifying(IAvaloniaObject o, bool updateStarted) private static void DataContextNotifying(IAvaloniaObject o, bool updateStarted)
{ {
if (o is StyledElement element) if (o is StyledElement element)

10
src/Avalonia.X11/X11KeyTransform.cs

@ -221,12 +221,8 @@ namespace Avalonia.X11
//{ X11Key.?, Key.DeadCharProcessed } //{ X11Key.?, Key.DeadCharProcessed }
}; };
public static Key ConvertKey(IntPtr key) public static Key ConvertKey(X11Key key)
{ => KeyDic.TryGetValue(key, out var result) ? result : Key.None;
var ikey = key.ToInt32(); }
Key result;
return KeyDic.TryGetValue((X11Key)ikey, out result) ? result : Key.None;
}
}
} }

15
src/Avalonia.X11/X11Window.cs

@ -419,10 +419,21 @@ namespace Avalonia.X11
return; return;
var buffer = stackalloc byte[40]; 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(), ScheduleInput(new RawKeyEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(),
ev.type == XEventName.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, 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) if (ev.type == XEventName.KeyPress)
{ {

31
src/Markup/Avalonia.Markup/Data/MultiBinding.cs

@ -64,14 +64,19 @@ namespace Avalonia.Data
object anchor = null, object anchor = null,
bool enableDataValidation = false) bool enableDataValidation = false)
{ {
if (Converter == null) var targetType = targetProperty?.PropertyType ?? typeof(object);
var converter = Converter;
// We only respect `StringFormat` if the type of the property we're assigning to will
// accept a string. Note that this is slightly different to WPF in that WPF only applies
// `StringFormat` for target type `string` (not `object`).
if (!string.IsNullOrWhiteSpace(StringFormat) &&
(targetType == typeof(string) || targetType == typeof(object)))
{ {
throw new NotSupportedException("MultiBinding without Converter not currently supported."); converter = new StringFormatMultiValueConverter(StringFormat, converter);
} }
var targetType = targetProperty?.PropertyType ?? typeof(object);
var children = Bindings.Select(x => x.Initiate(target, null)); var children = Bindings.Select(x => x.Initiate(target, null));
var input = children.Select(x => x.Observable).CombineLatest().Select(x => ConvertValue(x, targetType)); var input = children.Select(x => x.Observable).CombineLatest().Select(x => ConvertValue(x, targetType, converter));
var mode = Mode == BindingMode.Default ? var mode = Mode == BindingMode.Default ?
targetProperty?.GetMetadata(target.GetType()).DefaultBindingMode : Mode; targetProperty?.GetMetadata(target.GetType()).DefaultBindingMode : Mode;
@ -87,23 +92,19 @@ namespace Avalonia.Data
} }
} }
private object ConvertValue(IList<object> values, Type targetType) private object ConvertValue(IList<object> values, Type targetType, IMultiValueConverter converter)
{ {
var culture = CultureInfo.CurrentCulture; 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) if (converted == BindingOperations.DoNothing)
{ {
converted = FallbackValue; return converted;
} }
// We only respect `StringFormat` if the type of the property we're assigning to will if (converted == AvaloniaProperty.UnsetValue)
// 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); converted = FallbackValue;
} }
return converted; return converted;

37
tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs

@ -9,6 +9,7 @@ using Avalonia.Data;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Markup.Data; using Avalonia.Markup.Data;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.UnitTests;
using Xunit; using Xunit;
namespace Avalonia.Controls.UnitTests.Primitives namespace Avalonia.Controls.UnitTests.Primitives
@ -23,6 +24,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Minimum = 100, Minimum = 100,
Maximum = 50, Maximum = 50,
}; };
var root = new TestRoot(target);
Assert.Equal(100, target.Minimum); Assert.Equal(100, target.Minimum);
Assert.Equal(100, target.Maximum); Assert.Equal(100, target.Maximum);
@ -37,6 +39,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Maximum = 50, Maximum = 50,
Value = 100, Value = 100,
}; };
var root = new TestRoot(target);
Assert.Equal(0, target.Minimum); Assert.Equal(0, target.Minimum);
Assert.Equal(50, target.Maximum); Assert.Equal(50, target.Maximum);
@ -52,6 +55,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Maximum = 100, Maximum = 100,
Value = 50, Value = 50,
}; };
var root = new TestRoot(target);
target.Minimum = 200; target.Minimum = 200;
@ -69,6 +73,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Maximum = 100, Maximum = 100,
Value = 100, Value = 100,
}; };
var root = new TestRoot(target);
target.Maximum = 50; target.Maximum = 50;
@ -161,6 +166,38 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(expected, track.Value); 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 private class TestRange : RangeBase
{ {
} }

115
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -536,6 +536,9 @@ namespace Avalonia.Controls.UnitTests.Primitives
SelectedIndex = 1, SelectedIndex = 1,
}; };
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var called = false; var called = false;
target.SelectionChanged += (s, e) => target.SelectionChanged += (s, e) =>
@ -545,8 +548,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
called = true; called = true;
}; };
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
target.SelectedIndex = -1; target.SelectedIndex = -1;
Assert.True(called); Assert.True(called);
@ -783,6 +784,116 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(2, vm.Child.SelectedIndex); 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]);
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]);
Assert.Equal(new[] { ":selected" }, target.Presenter.Panel.Children[3].Classes);
}
[Fact]
public void Adding_Item_Before_SelectedItem_Should_Update_SelectedIndex()
{
var items = new ObservableCollection<string>
{
"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);
}
[Fact]
public void Removing_Item_Before_SelectedItem_Should_Update_SelectedIndex()
{
var items = new ObservableCollection<string>
{
"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);
}
[Fact]
public void Replacing_Selected_Item_Should_Update_SelectedItem()
{
var items = new ObservableCollection<string>
{
"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() private FuncControlTemplate Template()
{ {
return new FuncControlTemplate<SelectingItemsControl>(control => return new FuncControlTemplate<SelectingItemsControl>(control =>

523
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

@ -4,12 +4,15 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Data; using Avalonia.Markup.Data;
using Xunit; using Xunit;
@ -17,6 +20,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
{ {
public class SelectingItemsControlTests_Multiple public class SelectingItemsControlTests_Multiple
{ {
private MouseTestHelper _helper = new MouseTestHelper();
[Fact] [Fact]
public void Setting_SelectedIndex_Should_Add_To_SelectedItems() public void Setting_SelectedIndex_Should_Add_To_SelectedItems()
{ {
@ -258,31 +263,25 @@ namespace Avalonia.Controls.UnitTests.Primitives
} }
[Fact] [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 var target = new TestSelector
{ {
Items = items, Items = new[] { "foo", "bar", "baz" },
Template = Template(), Template = Template(),
}; };
target.ApplyTemplate(); target.ApplyTemplate();
target.Presenter.ApplyTemplate(); target.Presenter.ApplyTemplate();
target.SelectedIndex = 1;
target.SelectedItems[0] = items[2];
Assert.Equal(2, target.SelectedIndex); target.SelectedItems.Add("foo");
Assert.Equal(items[2], target.SelectedItem); target.SelectedItems.Add("bar");
Assert.False(items[0].IsSelected);
Assert.False(items[1].IsSelected); Assert.Equal(new[] { 0, 1 }, SelectedContainers(target));
Assert.True(items[2].IsSelected);
target.SelectedIndex = 2;
Assert.Equal(new[] { 2 }, SelectedContainers(target));
} }
[Fact] [Fact]
@ -361,6 +360,52 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(new[] { "baz", "qux", "qiz" }, target.SelectedItems.Cast<object>().ToList()); Assert.Equal(new[] { "baz", "qux", "qiz" }, target.SelectedItems.Cast<object>().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] [Fact]
public void Suprious_SelectedIndex_Changes_Should_Not_Be_Triggered() public void Suprious_SelectedIndex_Changes_Should_Not_Be_Triggered()
{ {
@ -382,6 +427,40 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(new[] { -1, 1, 0 }, selectedIndexes); 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));
}
/// <summary> /// <summary>
/// Tests a problem discovered with ListBox with selection. /// Tests a problem discovered with ListBox with selection.
/// </summary> /// </summary>
@ -471,6 +550,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{ {
DataContext = items, DataContext = items,
Template = Template(), Template = Template(),
Items = items,
}; };
var called = false; var called = false;
@ -540,35 +620,418 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.True(called); 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.Click((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] [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,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
_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.Click((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
{
Template = Template(),
Items = new[] { "Foo", "Bar", "Baz" },
SelectionMode = SelectionMode.Multiple,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
_helper.Click((Interactive)target.Presenter.Panel.Children[1]);
_helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control);
Assert.Equal(1, target.SelectedIndex);
Assert.Equal("Bar", target.SelectedItem);
_helper.Click((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
{
Template = Template(),
Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" },
SelectionMode = SelectionMode.Multiple,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
_helper.Click((Interactive)target.Presenter.Panel.Children[3]);
_helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: InputModifiers.Control);
var panel = target.Presenter.Panel;
Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems);
Assert.Equal(new[] { 3, 4 }, SelectedContainers(target));
}
[Fact]
public void Should_Shift_Select_Correct_Item_When_Duplicates_Are_Present()
{
var target = new ListBox
{
Template = Template(),
Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" },
SelectionMode = SelectionMode.Multiple,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
_helper.Click((Interactive)target.Presenter.Panel.Children[3]);
_helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: 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.Click((Interactive)target.Presenter.Panel.Children[0]);
_helper.Click((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.Click((Interactive)target.Presenter.Panel.Children[0]);
Assert.Equal(new[] { "Foo" }, target.SelectedItems);
_helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: InputModifiers.Control);
Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems);
_helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: InputModifiers.Control);
Assert.Equal(new[] { "Foo", "Bar", "Foo" }, target.SelectedItems);
_helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: InputModifiers.Control);
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()
{
var target = new TestSelector 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);
}
[Fact]
public void Adding_Item_Before_SelectedItems_Should_Update_Selection()
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"Baz"
};
var target = new ListBox
{
Template = Template(),
Items = items, 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);
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<string>
{
"Foo",
"Bar",
"Baz"
};
var target = new TestSelector
{
Template = Template(), Template = Template(),
SelectedItem = "bar", Items = items,
SelectionMode = SelectionMode.Multiple,
}; };
var called = false; target.ApplyTemplate();
target.Presenter.ApplyTemplate();
target.SelectionChanged += (s, e) => 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<string>
{ {
Assert.Equal(new[] { "foo",}, e.AddedItems.Cast<object>()); "Foo",
Assert.Equal(new[] { "bar" }, e.RemovedItems.Cast<object>()); "Bar",
called = true; "Baz"
};
var target = new ListBox
{
Template = Template(),
Items = items,
SelectionMode = SelectionMode.Multiple,
}; };
target.ApplyTemplate(); target.ApplyTemplate();
target.Presenter.ApplyTemplate(); target.Presenter.ApplyTemplate();
target.SelectedItems[0] = "foo";
Assert.True(called); 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<string>
{
"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);
}
[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<string>(x => new TextBlock { Width = 20, Height = 10 }),
SelectionMode = SelectionMode.Multiple,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
target.SelectAll();
Assert.Equal(3, target.SelectedItems.Count);
_helper.Click((Interactive)target.Presenter.Panel.Children[0]);
Assert.Equal(1, target.SelectedItems.Count);
Assert.Equal(new[] { "Foo", }, target.SelectedItems);
Assert.Equal(new[] { 0 }, SelectedContainers(target));
}
[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<string>(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<string>(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<int> SelectedContainers(SelectingItemsControl target)
{
return target.Presenter.Panel.Children
.Select((x, i) => x.Classes.Contains(":selected") ? i : -1)
.Where(x => x != -1);
}
private FuncControlTemplate Template() private FuncControlTemplate Template()
{ {
@ -598,10 +1061,10 @@ namespace Avalonia.Controls.UnitTests.Primitives
set { base.SelectionMode = value; } set { base.SelectionMode = value; }
} }
public void SelectRange(int index) public new void SelectAll() => base.SelectAll();
{ public new void UnselectAll() => base.UnselectAll();
UpdateSelection(index, true, true); public void SelectRange(int index) => UpdateSelection(index, true, true);
} public void Toggle(int index) => UpdateSelection(index, true, false, true);
} }
private class OldDataContextViewModel private class OldDataContextViewModel

40
tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests_Converters.cs

@ -5,11 +5,10 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Text;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Data.Core; using Avalonia.Layout;
using Xunit; using Xunit;
namespace Avalonia.Markup.UnitTests.Data namespace Avalonia.Markup.UnitTests.Data
@ -21,7 +20,30 @@ namespace Avalonia.Markup.UnitTests.Data
{ {
var textBlock = new TextBlock 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 var target = new MultiBinding
@ -30,8 +52,8 @@ namespace Avalonia.Markup.UnitTests.Data
Converter = new SumOfDoublesConverter(), Converter = new SumOfDoublesConverter(),
Bindings = Bindings =
{ {
new Binding(nameof(MultiBindingTests_Converters.Class1.Foo)), new Binding(nameof(Class1.Foo)),
new Binding(nameof(MultiBindingTests_Converters.Class1.Bar)), new Binding(nameof(Class1.Bar)),
} }
}; };
@ -45,7 +67,7 @@ namespace Avalonia.Markup.UnitTests.Data
{ {
var textBlock = new TextBlock var textBlock = new TextBlock
{ {
DataContext = new MultiBindingTests_Converters.Class1(), DataContext = new Class1(),
}; };
var target = new MultiBinding var target = new MultiBinding
@ -54,12 +76,12 @@ namespace Avalonia.Markup.UnitTests.Data
Converter = new SumOfDoublesConverter(), Converter = new SumOfDoublesConverter(),
Bindings = Bindings =
{ {
new Binding(nameof(MultiBindingTests_Converters.Class1.Foo)), new Binding(nameof(Class1.Foo)),
new Binding(nameof(MultiBindingTests_Converters.Class1.Bar)), new Binding(nameof(Class1.Bar)),
} }
}; };
textBlock.Bind(TextBlock.WidthProperty, target); textBlock.Bind(Layoutable.WidthProperty, target);
Assert.Equal(3.0, textBlock.Width); Assert.Equal(3.0, textBlock.Width);
} }

71
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 = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:c='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Converters;assembly=Avalonia.Markup.Xaml.UnitTests'>
<TextBlock Name='textBlock' Text='{Binding Converter={x:Static c:TestConverter.Instance}, FallbackValue=bar}'/>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("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();
}
}
}

39
tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs

@ -1,20 +1,10 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // 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.Controls;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Markup.Data;
using Moq;
using Xunit;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.Data.Converters; using Xunit;
using Avalonia.Data.Core;
namespace Avalonia.Markup.Xaml.UnitTests.Data namespace Avalonia.Markup.Xaml.UnitTests.Data
{ {
@ -40,5 +30,32 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
Assert.Equal("foo", textBlock.Text); Assert.Equal("foo", textBlock.Text);
} }
} }
[Fact]
public void Binding_To_DoNothing_Works()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<TextBlock Name='textBlock' Text='{Binding}'/>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("textBlock");
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);
}
}
} }
} }

19
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 = @"
<Window xmlns='https://github.com/avaloniaui'>
<Slider Width='400' Value='500' Minimum='0' Maximum='1000'/>
</Window>";
var window = AvaloniaXamlLoader.Parse<Window>(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 private class SelectedItemsViewModel : INotifyPropertyChanged
{ {
public string[] Items { get; set; } public string[] Items { get; set; }

31
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 = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<TextBlock Name='textBlock'>
<TextBlock.Text>
<MultiBinding StringFormat='\{0\} \{1\}!'>
<Binding Path='Greeting1'/>
<Binding Path='Greeting2'/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("textBlock");
textBlock.DataContext = new WindowViewModel();
window.ApplyTemplate();
Assert.Equal("Hello World!", textBlock.Text);
}
}
[Fact] [Fact]
public void Binding_OneWayToSource_Works() public void Binding_OneWayToSource_Works()
{ {
@ -356,6 +385,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
private class WindowViewModel private class WindowViewModel
{ {
public bool ShowInTaskbar { get; set; } public bool ShowInTaskbar { get; set; }
public string Greeting1 { get; set; } = "Hello";
public string Greeting2 { get; set; } = "World";
} }
} }
} }

98
tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs

@ -0,0 +1,98 @@
// 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 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;
namespace Avalonia.ReactiveUI.UnitTests
{
public class AutoSuspendHelperTest
{
[DataContract]
public class AppState
{
[DataMember]
public string Example { get; set; }
}
public class ExoticApplicationLifetimeWithoutLifecycleEvents : IDisposable, IApplicationLifetime
{
public void Dispose() { }
}
[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<Application>();
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);
}
}
[Fact]
public void ShouldPersistState_Should_Fire_On_App_Exit_When_SuspensionDriver_Is_Initialized()
{
using (UnitTestApplication.Start(TestServices.MockWindowingPlatform))
using (var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current))
{
var shouldPersistReceived = false;
var application = AvaloniaLocator.Current.GetService<Application>();
application.ApplicationLifetime = lifetime;
// 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();
lifetime.Shutdown();
Assert.True(shouldPersistReceived);
Assert.Equal("Foo", RxApp.SuspensionHost.GetAppState<AppState>().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>();
application.ApplicationLifetime = lifetime;
Assert.Throws<NotSupportedException>(() => new AutoSuspendHelper(application.ApplicationLifetime));
}
}
}
}

4
tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs

@ -171,7 +171,7 @@ namespace Avalonia.ReactiveUI.UnitTests
[Fact] [Fact]
public void Activation_For_View_Fetcher_Should_Support_Windows() 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(); var window = new TestWindowWithWhenActivated();
Assert.False(window.Active); Assert.False(window.Active);
@ -187,7 +187,7 @@ namespace Avalonia.ReactiveUI.UnitTests
[Fact] [Fact]
public void Activatable_Window_View_Model_Is_Activated_And_Deactivated() 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 viewModel = new ActivatableViewModel();
var window = new ActivatableWindow { ViewModel = viewModel }; var window = new ActivatableWindow { ViewModel = viewModel };

Loading…
Cancel
Save