diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs
index 15de6c4d0d..44b47329ac 100644
--- a/src/Avalonia.Base/Data/BindingOperations.cs
+++ b/src/Avalonia.Base/Data/BindingOperations.cs
@@ -10,6 +10,8 @@ namespace Avalonia.Data
{
public static class BindingOperations
{
+ public static readonly object DoNothing = new object();
+
///
/// Applies an a property on an .
///
diff --git a/src/Avalonia.Base/Data/Converters/StringFormatMultiValueConverter.cs b/src/Avalonia.Base/Data/Converters/StringFormatMultiValueConverter.cs
new file mode 100644
index 0000000000..b190f06be5
--- /dev/null
+++ b/src/Avalonia.Base/Data/Converters/StringFormatMultiValueConverter.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+
+namespace Avalonia.Data.Converters
+{
+ ///
+ /// A multi-value converter which calls
+ ///
+ public class StringFormatMultiValueConverter : IMultiValueConverter
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The format string.
+ ///
+ /// An optional inner converter to be called before the format takes place.
+ ///
+ public StringFormatMultiValueConverter(string format, IMultiValueConverter inner)
+ {
+ Contract.Requires(format != null);
+
+ Format = format;
+ Inner = inner;
+ }
+
+ ///
+ /// Gets an inner value converter which will be called before the string format takes place.
+ ///
+ public IMultiValueConverter Inner { get; }
+
+ ///
+ /// Gets the format string.
+ ///
+ public string Format { get; }
+
+ ///
+ public object Convert(IList values, Type targetType, object parameter, CultureInfo culture)
+ {
+ return Inner == null
+ ? string.Format(culture, Format, values.ToArray())
+ : string.Format(culture, Format, Inner.Convert(values, targetType, parameter, culture));
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs
index f1717bde3b..7f8396cdfa 100644
--- a/src/Avalonia.Base/Data/Core/BindingExpression.cs
+++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs
@@ -114,6 +114,11 @@ namespace Avalonia.Data.Core
///
public void OnNext(object value)
{
+ if (value == BindingOperations.DoNothing)
+ {
+ return;
+ }
+
using (_inner.Subscribe(_ => { }))
{
var type = _inner.ResultType;
@@ -126,6 +131,11 @@ namespace Avalonia.Data.Core
ConverterParameter,
CultureInfo.CurrentCulture);
+ if (converted == BindingOperations.DoNothing)
+ {
+ return;
+ }
+
if (converted == AvaloniaProperty.UnsetValue)
{
converted = TypeUtilities.Default(type);
@@ -186,6 +196,11 @@ namespace Avalonia.Data.Core
///
private object ConvertValue(object value)
{
+ if (value == BindingOperations.DoNothing)
+ {
+ return value;
+ }
+
var notification = value as BindingNotification;
if (notification == null)
@@ -196,6 +211,11 @@ namespace Avalonia.Data.Core
ConverterParameter,
CultureInfo.CurrentCulture);
+ if (converted == BindingOperations.DoNothing)
+ {
+ return converted;
+ }
+
notification = converted as BindingNotification;
if (notification?.ErrorType == BindingErrorType.None)
@@ -327,7 +347,18 @@ namespace Avalonia.Data.Core
public void OnNext(object value)
{
+ if (value == BindingOperations.DoNothing)
+ {
+ return;
+ }
+
var converted = _owner.ConvertValue(value);
+
+ if (converted == BindingOperations.DoNothing)
+ {
+ return;
+ }
+
_owner._value = new WeakReference(converted);
_owner.PublishNext(converted);
}
diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs
index 0f0b55e9e8..59ff35be76 100644
--- a/src/Avalonia.Controls/AppBuilderBase.cs
+++ b/src/Avalonia.Controls/AppBuilderBase.cs
@@ -4,6 +4,7 @@
using System;
using System.Reflection;
using System.Linq;
+using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform;
namespace Avalonia.Controls
@@ -106,19 +107,28 @@ namespace Avalonia.Controls
public void Start(Func dataContextProvider = null)
where TMainWindow : Window, new()
{
- var window = new TMainWindow();
- if (dataContextProvider != null)
- window.DataContext = dataContextProvider();
- Instance.Run(window);
+ AfterSetup(builder =>
+ {
+ var window = new TMainWindow();
+ if (dataContextProvider != null)
+ window.DataContext = dataContextProvider();
+ ((IClassicDesktopStyleApplicationLifetime)builder.Instance.ApplicationLifetime)
+ .MainWindow = window;
+ });
+
+ // Copy-pasted because we can't call extension methods due to generic constraints
+ var lifetime = new ClassicDesktopStyleApplicationLifetime(Instance) {ShutdownMode = ShutdownMode.OnMainWindowClose};
+ Instance.ApplicationLifetime = lifetime;
+ SetupWithoutStarting();
+ lifetime.Start(Array.Empty());
}
public delegate void AppMainDelegate(Application app, string[] args);
- [Obsolete("Use either lifetimes or AppMain overload. See see https://github.com/AvaloniaUI/Avalonia/wiki/Application-lifetimes for details")]
+ [Obsolete("Use either lifetimes or AppMain overload. See see https://github.com/AvaloniaUI/Avalonia/wiki/Application-lifetimes for details", true)]
public void Start()
{
- Setup();
- Instance.Run();
+ throw new NotSupportedException();
}
public void Start(AppMainDelegate main, string[] args)
diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs b/src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs
index d4c3b27f7a..2963c019cc 100644
--- a/src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs
+++ b/src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs
@@ -6,7 +6,7 @@ using System;
namespace Avalonia.Controls.ApplicationLifetimes
{
///
- /// Contains the arguments for the event.
+ /// Contains the arguments for the event.
///
public class ControlledApplicationLifetimeExitEventArgs : EventArgs
{
diff --git a/src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs b/src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs
index 4c08712707..423832793e 100644
--- a/src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs
+++ b/src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs
@@ -8,7 +8,7 @@ using System.Linq;
namespace Avalonia.Controls.ApplicationLifetimes
{
///
- /// Contains the arguments for the event.
+ /// Contains the arguments for the event.
///
public class ControlledApplicationLifetimeStartupEventArgs : EventArgs
{
diff --git a/src/Avalonia.Controls/ColumnDefinition.cs b/src/Avalonia.Controls/ColumnDefinition.cs
index e3d2489241..9c520c434e 100644
--- a/src/Avalonia.Controls/ColumnDefinition.cs
+++ b/src/Avalonia.Controls/ColumnDefinition.cs
@@ -62,8 +62,15 @@ namespace Avalonia.Controls
///
public double MaxWidth
{
- get { return GetValue(MaxWidthProperty); }
- set { SetValue(MaxWidthProperty, value); }
+ get
+ {
+ return GetValue(MaxWidthProperty);
+ }
+ set
+ {
+ Parent?.InvalidateMeasure();
+ SetValue(MaxWidthProperty, value);
+ }
}
///
@@ -71,8 +78,15 @@ namespace Avalonia.Controls
///
public double MinWidth
{
- get { return GetValue(MinWidthProperty); }
- set { SetValue(MinWidthProperty, value); }
+ get
+ {
+ return GetValue(MinWidthProperty);
+ }
+ set
+ {
+ Parent?.InvalidateMeasure();
+ SetValue(MinWidthProperty, value);
+ }
}
///
@@ -80,8 +94,15 @@ namespace Avalonia.Controls
///
public GridLength Width
{
- get { return GetValue(WidthProperty); }
- set { SetValue(WidthProperty, value); }
+ get
+ {
+ return GetValue(WidthProperty);
+ }
+ set
+ {
+ Parent?.InvalidateMeasure();
+ SetValue(WidthProperty, value);
+ }
}
internal override GridLength UserSizeValueCache => this.Width;
diff --git a/src/Avalonia.Controls/DesktopApplicationExtensions.cs b/src/Avalonia.Controls/DesktopApplicationExtensions.cs
index 9b81bc94d1..ff6705cdc0 100644
--- a/src/Avalonia.Controls/DesktopApplicationExtensions.cs
+++ b/src/Avalonia.Controls/DesktopApplicationExtensions.cs
@@ -8,7 +8,7 @@ namespace Avalonia.Controls
{
public static class DesktopApplicationExtensions
{
- [Obsolete("Running application without a cancellation token and a lifetime is no longer supported, see https://github.com/AvaloniaUI/Avalonia/wiki/Application-lifetimes for details")]
+ [Obsolete("Running application without a cancellation token and a lifetime is no longer supported, see https://github.com/AvaloniaUI/Avalonia/wiki/Application-lifetimes for details", true)]
public static void Run(this Application app) => throw new NotSupportedException();
///
diff --git a/src/Avalonia.Controls/DockPanel.cs b/src/Avalonia.Controls/DockPanel.cs
index e147fe1a52..8e23555c2d 100644
--- a/src/Avalonia.Controls/DockPanel.cs
+++ b/src/Avalonia.Controls/DockPanel.cs
@@ -1,7 +1,12 @@
+// This source file is adapted from the Windows Presentation Foundation project.
+// (https://github.com/dotnet/wpf/)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+
namespace Avalonia.Controls
{
- using System;
-
///
/// Defines the available docking modes for a control in a .
///
@@ -70,107 +75,137 @@ namespace Avalonia.Controls
set { SetValue(LastChildFillProperty, value); }
}
- ///
+ ///
+ /// Updates DesiredSize of the DockPanel. Called by parent Control. This is the first pass of layout.
+ ///
+ ///
+ /// Children are measured based on their sizing properties and .
+ /// Each child is allowed to consume all of the space on the side on which it is docked; Left/Right docked
+ /// children are granted all vertical space for their entire width, and Top/Bottom docked children are
+ /// granted all horizontal space for their entire height.
+ ///
+ /// Constraint size is an "upper limit" that the return value should not exceed.
+ /// The Panel's desired size.
protected override Size MeasureOverride(Size constraint)
{
- double usedWidth = 0.0;
- double usedHeight = 0.0;
- double maximumWidth = 0.0;
- double maximumHeight = 0.0;
+ var children = Children;
+
+ double parentWidth = 0; // Our current required width due to children thus far.
+ double parentHeight = 0; // Our current required height due to children thus far.
+ double accumulatedWidth = 0; // Total width consumed by children.
+ double accumulatedHeight = 0; // Total height consumed by children.
- // Measure each of the Children
- foreach (Control element in Children)
+ for (int i = 0, count = children.Count; i < count; ++i)
{
- // Get the child's desired size
- Size remainingSize = new Size(
- Math.Max(0.0, constraint.Width - usedWidth),
- Math.Max(0.0, constraint.Height - usedHeight));
- element.Measure(remainingSize);
- Size desiredSize = element.DesiredSize;
-
- // Decrease the remaining space for the rest of the children
- switch (GetDock(element))
+ var child = children[i];
+ Size childConstraint; // Contains the suggested input constraint for this child.
+ Size childDesiredSize; // Contains the return size from child measure.
+
+ if (child == null)
+ { continue; }
+
+ // Child constraint is the remaining size; this is total size minus size consumed by previous children.
+ childConstraint = new Size(Math.Max(0.0, constraint.Width - accumulatedWidth),
+ Math.Max(0.0, constraint.Height - accumulatedHeight));
+
+ // Measure child.
+ child.Measure(childConstraint);
+ childDesiredSize = child.DesiredSize;
+
+ // Now, we adjust:
+ // 1. Size consumed by children (accumulatedSize). This will be used when computing subsequent
+ // children to determine how much space is remaining for them.
+ // 2. Parent size implied by this child (parentSize) when added to the current children (accumulatedSize).
+ // This is different from the size above in one respect: A Dock.Left child implies a height, but does
+ // not actually consume any height for subsequent children.
+ // If we accumulate size in a given dimension, the next child (or the end conditions after the child loop)
+ // will deal with computing our minimum size (parentSize) due to that accumulation.
+ // Therefore, we only need to compute our minimum size (parentSize) in dimensions that this child does
+ // not accumulate: Width for Top/Bottom, Height for Left/Right.
+ switch (DockPanel.GetDock((Control)child))
{
case Dock.Left:
case Dock.Right:
- maximumHeight = Math.Max(maximumHeight, usedHeight + desiredSize.Height);
- usedWidth += desiredSize.Width;
+ parentHeight = Math.Max(parentHeight, accumulatedHeight + childDesiredSize.Height);
+ accumulatedWidth += childDesiredSize.Width;
break;
+
case Dock.Top:
case Dock.Bottom:
- maximumWidth = Math.Max(maximumWidth, usedWidth + desiredSize.Width);
- usedHeight += desiredSize.Height;
+ parentWidth = Math.Max(parentWidth, accumulatedWidth + childDesiredSize.Width);
+ accumulatedHeight += childDesiredSize.Height;
break;
}
}
- maximumWidth = Math.Max(maximumWidth, usedWidth);
- maximumHeight = Math.Max(maximumHeight, usedHeight);
- return new Size(maximumWidth, maximumHeight);
+ // Make sure the final accumulated size is reflected in parentSize.
+ parentWidth = Math.Max(parentWidth, accumulatedWidth);
+ parentHeight = Math.Max(parentHeight, accumulatedHeight);
+
+ return (new Size(parentWidth, parentHeight));
}
- ///
+ ///
+ /// DockPanel computes a position and final size for each of its children based upon their
+ /// enum and sizing properties.
+ ///
+ /// Size that DockPanel will assume to position children.
protected override Size ArrangeOverride(Size arrangeSize)
{
- double left = 0.0;
- double top = 0.0;
- double right = 0.0;
- double bottom = 0.0;
-
- // Arrange each of the Children
var children = Children;
- int dockedCount = children.Count - (LastChildFill ? 1 : 0);
- int index = 0;
+ int totalChildrenCount = children.Count;
+ int nonFillChildrenCount = totalChildrenCount - (LastChildFill ? 1 : 0);
+
+ 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
- Rect remainingRect = new Rect(
- left,
- top,
- Math.Max(0.0, arrangeSize.Width - left - right),
- Math.Max(0.0, arrangeSize.Height - top - bottom));
-
- // Trim the remaining Rect to the docked size of the element
- // (unless the element should fill the remaining space because
- // of LastChildFill)
- if (index < dockedCount)
+ var child = children[i];
+ if (child == null)
+ { continue; }
+
+ Size childDesiredSize = child.DesiredSize;
+ Rect rcChild = new Rect(
+ accumulatedLeft,
+ accumulatedTop,
+ Math.Max(0.0, arrangeSize.Width - (accumulatedLeft + accumulatedRight)),
+ Math.Max(0.0, arrangeSize.Height - (accumulatedTop + accumulatedBottom)));
+
+ if (i < nonFillChildrenCount)
{
- Size desiredSize = element.DesiredSize;
- switch (GetDock(element))
+ switch (DockPanel.GetDock((Control)child))
{
case Dock.Left:
- left += desiredSize.Width;
- remainingRect = remainingRect.WithWidth(desiredSize.Width);
- break;
- case Dock.Top:
- top += desiredSize.Height;
- remainingRect = remainingRect.WithHeight(desiredSize.Height);
+ accumulatedLeft += childDesiredSize.Width;
+ rcChild = rcChild.WithWidth(childDesiredSize.Width);
break;
+
case Dock.Right:
- right += desiredSize.Width;
- remainingRect = new Rect(
- Math.Max(0.0, arrangeSize.Width - right),
- remainingRect.Y,
- desiredSize.Width,
- remainingRect.Height);
+ accumulatedRight += childDesiredSize.Width;
+ rcChild = rcChild.WithX(Math.Max(0.0, arrangeSize.Width - accumulatedRight));
+ rcChild = rcChild.WithWidth(childDesiredSize.Width);
break;
+
+ case Dock.Top:
+ accumulatedTop += childDesiredSize.Height;
+ rcChild = rcChild.WithHeight(childDesiredSize.Height);
+ break;
+
case Dock.Bottom:
- bottom += desiredSize.Height;
- remainingRect = new Rect(
- remainingRect.X,
- Math.Max(0.0, arrangeSize.Height - bottom),
- remainingRect.Width,
- desiredSize.Height);
+ accumulatedBottom += childDesiredSize.Height;
+ rcChild = rcChild.WithY(Math.Max(0.0, arrangeSize.Height - accumulatedBottom));
+ rcChild = rcChild.WithHeight(childDesiredSize.Height);
break;
}
}
- element.Arrange(remainingRect);
- index++;
+ child.Arrange(rcChild);
}
- return arrangeSize;
+ return (arrangeSize);
}
}
}
diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs
index 3dfeae52a4..a292ff7d0a 100644
--- a/src/Avalonia.Controls/ItemsControl.cs
+++ b/src/Avalonia.Controls/ItemsControl.cs
@@ -36,6 +36,12 @@ namespace Avalonia.Controls
public static readonly DirectProperty ItemsProperty =
AvaloniaProperty.RegisterDirect(nameof(Items), o => o.Items, (o, v) => o.Items = v);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty ItemCountProperty =
+ AvaloniaProperty.RegisterDirect(nameof(ItemCount), o => o.ItemCount);
+
///
/// Defines the property.
///
@@ -55,6 +61,7 @@ namespace Avalonia.Controls
AvaloniaProperty.Register(nameof(MemberSelector));
private IEnumerable _items = new AvaloniaList();
+ private int _itemCount;
private IItemContainerGenerator _itemContainerGenerator;
private IDisposable _itemsCollectionChangedSubscription;
@@ -110,10 +117,13 @@ namespace Avalonia.Controls
set { SetAndRaise(ItemsProperty, ref _items, value); }
}
+ ///
+ /// Gets the number of items in .
+ ///
public int ItemCount
{
- get;
- private set;
+ get => _itemCount;
+ private set => SetAndRaise(ItemCountProperty, ref _itemCount, value);
}
///
diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs
index 041b81155a..3150b6be91 100644
--- a/src/Avalonia.Controls/ListBox.cs
+++ b/src/Avalonia.Controls/ListBox.cs
@@ -84,6 +84,16 @@ namespace Avalonia.Controls
set { SetValue(VirtualizationModeProperty, value); }
}
+ ///
+ /// Selects all items in the .
+ ///
+ public new void SelectAll() => base.SelectAll();
+
+ ///
+ /// Deselects all items in the .
+ ///
+ public new void UnselectAll() => base.UnselectAll();
+
///
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
@@ -118,7 +128,8 @@ namespace Avalonia.Controls
e.Source,
true,
(e.InputModifiers & InputModifiers.Shift) != 0,
- (e.InputModifiers & InputModifiers.Control) != 0);
+ (e.InputModifiers & InputModifiers.Control) != 0,
+ e.MouseButton == MouseButton.Right);
}
}
diff --git a/src/Avalonia.Controls/Primitives/RangeBase.cs b/src/Avalonia.Controls/Primitives/RangeBase.cs
index ae175734b9..f1ee7c0e1a 100644
--- a/src/Avalonia.Controls/Primitives/RangeBase.cs
+++ b/src/Avalonia.Controls/Primitives/RangeBase.cs
@@ -75,10 +75,18 @@ namespace Avalonia.Controls.Primitives
set
{
- value = ValidateMinimum(value);
- SetAndRaise(MinimumProperty, ref _minimum, value);
- Maximum = ValidateMaximum(Maximum);
- Value = ValidateValue(Value);
+ ValidateDouble(value, "Minimum");
+
+ if (IsInitialized)
+ {
+ SetAndRaise(MinimumProperty, ref _minimum, value);
+ Maximum = ValidateMaximum(Maximum);
+ Value = ValidateValue(Value);
+ }
+ else
+ {
+ SetAndRaise(MinimumProperty, ref _minimum, value);
+ }
}
}
@@ -94,9 +102,18 @@ namespace Avalonia.Controls.Primitives
set
{
- value = ValidateMaximum(value);
- SetAndRaise(MaximumProperty, ref _maximum, value);
- Value = ValidateValue(Value);
+ ValidateDouble(value, "Maximum");
+
+ if (IsInitialized)
+ {
+ value = ValidateMaximum(value);
+ SetAndRaise(MaximumProperty, ref _maximum, value);
+ Value = ValidateValue(Value);
+ }
+ else
+ {
+ SetAndRaise(MaximumProperty, ref _maximum, value);
+ }
}
}
@@ -112,8 +129,17 @@ namespace Avalonia.Controls.Primitives
set
{
- value = ValidateValue(value);
- SetAndRaise(ValueProperty, ref _value, value);
+ ValidateDouble(value, "Value");
+
+ if (IsInitialized)
+ {
+ value = ValidateValue(value);
+ SetAndRaise(ValueProperty, ref _value, value);
+ }
+ else
+ {
+ SetAndRaise(ValueProperty, ref _value, value);
+ }
}
}
@@ -129,6 +155,14 @@ namespace Avalonia.Controls.Primitives
set => SetValue(LargeChangeProperty, value);
}
+ protected override void OnInitialized()
+ {
+ base.OnInitialized();
+
+ Maximum = ValidateMaximum(Maximum);
+ Value = ValidateValue(Value);
+ }
+
///
/// Throws an exception if the double value is NaN or Inf.
///
@@ -142,17 +176,6 @@ namespace Avalonia.Controls.Primitives
}
}
- ///
- /// Validates the property.
- ///
- /// The value.
- /// The coerced value.
- private double ValidateMinimum(double value)
- {
- ValidateDouble(value, "Minimum");
- return value;
- }
-
///
/// Validates/coerces the property.
///
@@ -160,7 +183,6 @@ namespace Avalonia.Controls.Primitives
/// The coerced value.
private double ValidateMaximum(double value)
{
- ValidateDouble(value, "Maximum");
return Math.Max(value, Minimum);
}
@@ -171,7 +193,6 @@ namespace Avalonia.Controls.Primitives
/// The coerced value.
private double ValidateValue(double value)
{
- ValidateDouble(value, "Value");
return MathUtilities.Clamp(value, Minimum, Maximum);
}
}
diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
index 7b91d6235d..91a9fa7e40 100644
--- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
+++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
@@ -12,6 +12,7 @@ using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
+using Avalonia.Logging;
using Avalonia.Styling;
using Avalonia.VisualTree;
@@ -103,6 +104,7 @@ namespace Avalonia.Controls.Primitives
private static readonly IList Empty = Array.Empty();
+ private readonly Selection _selection = new Selection();
private int _selectedIndex = -1;
private object _selectedItem;
private IList _selectedItems;
@@ -152,23 +154,8 @@ namespace Avalonia.Controls.Primitives
{
if (_updateCount == 0)
{
- SetAndRaise(SelectedIndexProperty, ref _selectedIndex, (int val, ref int backing, Action notifyWrapper) =>
- {
- var old = backing;
- var effective = (val >= 0 && val < Items?.Cast().Count()) ? val : -1;
-
- if (old != effective)
- {
- backing = effective;
- notifyWrapper(() =>
- RaisePropertyChanged(
- SelectedIndexProperty,
- old,
- effective,
- BindingPriority.LocalValue));
- SelectedItem = ElementAt(Items, effective);
- }
- }, value);
+ var effective = (value >= 0 && value < ItemCount) ? value : -1;
+ UpdateSelectedItem(effective);
}
else
{
@@ -192,41 +179,7 @@ namespace Avalonia.Controls.Primitives
{
if (_updateCount == 0)
{
- SetAndRaise(SelectedItemProperty, ref _selectedItem, (object val, ref object backing, Action notifyWrapper) =>
- {
- var old = backing;
- var index = IndexOf(Items, val);
- var effective = index != -1 ? val : null;
-
- if (!object.Equals(effective, old))
- {
- backing = effective;
-
- notifyWrapper(() =>
- RaisePropertyChanged(
- SelectedItemProperty,
- old,
- effective,
- BindingPriority.LocalValue));
-
- SelectedIndex = index;
-
- if (effective != null)
- {
- if (SelectedItems.Count != 1 || SelectedItems[0] != effective)
- {
- _syncingSelectedItems = true;
- SelectedItems.Clear();
- SelectedItems.Add(effective);
- _syncingSelectedItems = false;
- }
- }
- else if (SelectedItems.Count > 0)
- {
- SelectedItems.Clear();
- }
- }
- }, value);
+ UpdateSelectedItem(IndexOf(Items, value));
}
else
{
@@ -354,31 +307,23 @@ namespace Avalonia.Controls.Primitives
{
SelectedIndex = 0;
}
+ else
+ {
+ _selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count);
+ UpdateSelectedItem(_selection.First(), false);
+ }
break;
case NotifyCollectionChangedAction.Remove:
- case NotifyCollectionChangedAction.Replace:
- var selectedIndex = SelectedIndex;
-
- if (selectedIndex >= e.OldStartingIndex &&
- selectedIndex < e.OldStartingIndex + e.OldItems.Count)
- {
- if (!AlwaysSelected)
- {
- selectedIndex = SelectedIndex = -1;
- }
- else
- {
- LostSelection();
- }
- }
+ _selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count);
+ UpdateSelectedItem(_selection.First(), false);
+ ResetSelectedItems();
+ break;
- var items = Items?.Cast();
- if (selectedIndex >= items.Count())
- {
- selectedIndex = SelectedIndex = items.Count() - 1;
- }
+ case NotifyCollectionChangedAction.Replace:
+ UpdateSelectedItem(SelectedIndex, false);
+ ResetSelectedItems();
break;
case NotifyCollectionChangedAction.Move:
@@ -439,11 +384,7 @@ namespace Avalonia.Controls.Primitives
{
if (i.ContainerControl != null && i.Item != null)
{
- var ms = MemberSelector;
- bool selected = ms == null ?
- SelectedItems.Contains(i.Item) :
- SelectedItems.OfType().Any(v => Equals(ms.Select(v), i.Item));
-
+ bool selected = _selection.Contains(i.Index);
MarkContainerSelected(i.ContainerControl, selected);
}
}
@@ -476,9 +417,12 @@ namespace Avalonia.Controls.Primitives
var keymap = AvaloniaLocator.Current.GetService();
bool Match(List gestures) => gestures.Any(g => g.Matches(e));
- if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll))
+ if (ItemCount > 0 &&
+ Match(keymap.SelectAll) &&
+ (((SelectionMode & SelectionMode.Multiple) != 0) ||
+ (SelectionMode & SelectionMode.Toggle) != 0))
{
- SynchronizeItems(SelectedItems, Items?.Cast());
+ SelectAll();
e.Handled = true;
}
}
@@ -520,6 +464,41 @@ namespace Avalonia.Controls.Primitives
return false;
}
+ ///
+ /// Selects all items in the control.
+ ///
+ protected void SelectAll()
+ {
+ if ((SelectionMode & (SelectionMode.Multiple | SelectionMode.Toggle)) == 0)
+ {
+ throw new NotSupportedException("Multiple selection is not enabled on this control.");
+ }
+
+ UpdateSelectedItems(() =>
+ {
+ _selection.Clear();
+
+ for (var i = 0; i < ItemCount; ++i)
+ {
+ _selection.Add(i);
+ }
+
+ UpdateSelectedItem(0, false);
+
+ foreach (var container in ItemContainerGenerator.Containers)
+ {
+ MarkItemSelected(container.Index, true);
+ }
+
+ ResetSelectedItems();
+ });
+ }
+
+ ///
+ /// Deselects all items in the control.
+ ///
+ protected void UnselectAll() => UpdateSelectedItem(-1);
+
///
/// Updates the selection for an item based on user interaction.
///
@@ -527,51 +506,83 @@ namespace Avalonia.Controls.Primitives
/// Whether the item should be selected or unselected.
/// Whether the range modifier is enabled (i.e. shift key).
/// Whether the toggle modifier is enabled (i.e. ctrl key).
+ /// Whether the event is a right-click.
protected void UpdateSelection(
int index,
bool select = true,
bool rangeModifier = false,
- bool toggleModifier = false)
+ bool toggleModifier = false,
+ bool rightButton = false)
{
if (index != -1)
{
if (select)
{
var mode = SelectionMode;
- var toggle = toggleModifier || (mode & SelectionMode.Toggle) != 0;
var multi = (mode & SelectionMode.Multiple) != 0;
- var range = multi && SelectedIndex != -1 && rangeModifier;
+ var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0);
+ var range = multi && rangeModifier;
- if (!toggle && !range)
+ if (range)
{
- SelectedIndex = index;
- }
- else if (multi && range)
- {
- SynchronizeItems(
- SelectedItems,
- GetRange(Items, SelectedIndex, index));
+ UpdateSelectedItems(() =>
+ {
+ var start = SelectedIndex != -1 ? SelectedIndex : 0;
+ var step = start < index ? 1 : -1;
+
+ _selection.Clear();
+
+ 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);
- var i = SelectedItems.IndexOf(item);
-
- if (i != -1 && (!AlwaysSelected || SelectedItems.Count > 1))
- {
- SelectedItems.Remove(item);
- }
- else
+ UpdateSelectedItems(() =>
{
- if (multi)
+ if (!_selection.Contains(index))
{
- SelectedItems.Add(item);
+ _selection.Add(index);
+ MarkItemSelected(index, true);
+ SelectedItems.Add(ElementAt(Items, index));
}
else
{
- SelectedIndex = index;
+ _selection.Remove(index);
+ MarkItemSelected(index, false);
+
+ if (index == _selectedIndex)
+ {
+ UpdateSelectedItem(_selection.First(), false);
+ }
+
+ SelectedItems.Remove(ElementAt(Items, index));
}
- }
+ });
+ }
+ else if (toggle)
+ {
+ SelectedIndex = (SelectedIndex == index) ? -1 : index;
+ }
+ else
+ {
+ UpdateSelectedItem(index, !(rightButton && _selection.Contains(index)));
}
if (Presenter?.Panel != null)
@@ -596,17 +607,19 @@ namespace Avalonia.Controls.Primitives
/// Whether the container should be selected or unselected.
/// Whether the range modifier is enabled (i.e. shift key).
/// Whether the toggle modifier is enabled (i.e. ctrl key).
+ /// Whether the event is a right-click.
protected void UpdateSelection(
IControl container,
bool select = true,
bool rangeModifier = false,
- bool toggleModifier = false)
+ bool toggleModifier = false,
+ bool rightButton = false)
{
var index = ItemContainerGenerator?.IndexFromContainer(container) ?? -1;
if (index != -1)
{
- UpdateSelection(index, select, rangeModifier, toggleModifier);
+ UpdateSelection(index, select, rangeModifier, toggleModifier, rightButton);
}
}
@@ -618,6 +631,7 @@ namespace Avalonia.Controls.Primitives
/// Whether the container should be selected or unselected.
/// Whether the range modifier is enabled (i.e. shift key).
/// Whether the toggle modifier is enabled (i.e. ctrl key).
+ /// Whether the event is a right-click.
///
/// True if the event originated from a container that belongs to the control; otherwise
/// false.
@@ -626,51 +640,20 @@ namespace Avalonia.Controls.Primitives
IInteractive eventSource,
bool select = true,
bool rangeModifier = false,
- bool toggleModifier = false)
+ bool toggleModifier = false,
+ bool rightButton = false)
{
var container = GetContainerFromEventSource(eventSource);
if (container != null)
{
- UpdateSelection(container, select, rangeModifier, toggleModifier);
+ UpdateSelection(container, select, rangeModifier, toggleModifier, rightButton);
return true;
}
return false;
}
- ///
- /// Makes a list of objects equal another.
- ///
- /// The items collection.
- /// The desired items.
- internal static void SynchronizeItems(IList items, IEnumerable 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);
- }
- }
-
///
/// Gets a range of items from an IEnumerable.
///
@@ -678,17 +661,19 @@ namespace Avalonia.Controls.Primitives
/// The index of the first item.
/// The index of the last item.
/// The items.
- private static IEnumerable GetRange(IEnumerable items, int first, int last)
+ private static List GetRange(IEnumerable items, int first, int last)
{
var list = (items as IList) ?? items.Cast().ToList();
- int step = first > last ? -1 : 1;
+ var step = first > last ? -1 : 1;
+ var result = new List();
for (int i = first; i != last; i += step)
{
- yield return list[i];
+ result.Add(list[i]);
}
- yield return list[last];
+ result.Add(list[last]);
+ return result;
}
///
@@ -724,19 +709,14 @@ namespace Avalonia.Controls.Primitives
private void LostSelection()
{
var items = Items?.Cast();
+ var index = -1;
if (items != null && AlwaysSelected)
{
- var index = Math.Min(SelectedIndex, items.Count() - 1);
-
- if (index > -1)
- {
- SelectedItem = items.ElementAt(index);
- return;
- }
+ index = Math.Min(SelectedIndex, items.Count() - 1);
}
- SelectedIndex = -1;
+ SelectedIndex = index;
}
///
@@ -793,7 +773,7 @@ namespace Avalonia.Controls.Primitives
///
/// The item.
/// Whether the item should be selected or deselected.
- private void MarkItemSelected(object item, bool selected)
+ private int MarkItemSelected(object item, bool selected)
{
var index = IndexOf(Items, item);
@@ -801,6 +781,21 @@ namespace Avalonia.Controls.Primitives
{
MarkItemSelected(index, selected);
}
+
+ return index;
+ }
+
+ private void ResetSelectedItems()
+ {
+ UpdateSelectedItems(() =>
+ {
+ SelectedItems.Clear();
+
+ foreach (var i in _selection)
+ {
+ SelectedItems.Add(ElementAt(Items, i));
+ }
+ });
}
///
@@ -810,95 +805,97 @@ namespace Avalonia.Controls.Primitives
/// The event args.
private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
- var generator = ItemContainerGenerator;
+ if (_syncingSelectedItems)
+ {
+ return;
+ }
+
+ void Add(IList newItems, IList addedItems = null)
+ {
+ foreach (var item in newItems)
+ {
+ var index = MarkItemSelected(item, true);
+
+ if (index != -1 && _selection.Add(index) && addedItems != null)
+ {
+ addedItems.Add(item);
+ }
+ }
+ }
+
+ void UpdateSelection()
+ {
+ if ((SelectedIndex != -1 && !_selection.Contains(SelectedIndex)) ||
+ (SelectedIndex == -1 && _selection.HasItems))
+ {
+ _selectedIndex = _selection.First();
+ _selectedItem = ElementAt(Items, _selectedIndex);
+ RaisePropertyChanged(SelectedIndexProperty, -1, _selectedIndex, BindingPriority.LocalValue);
+ RaisePropertyChanged(SelectedItemProperty, null, _selectedItem, BindingPriority.LocalValue);
+
+ if (AutoScrollToSelectedItem)
+ {
+ ScrollIntoView(_selectedIndex);
+ }
+ }
+ }
+
IList added = null;
IList removed = null;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
- SelectedItemsAdded(e.NewItems.Cast().ToList());
-
- if (AutoScrollToSelectedItem)
{
- ScrollIntoView(e.NewItems[0]);
+ Add(e.NewItems);
+ UpdateSelection();
+ added = e.NewItems;
}
- added = e.NewItems;
break;
case NotifyCollectionChangedAction.Remove:
if (SelectedItems.Count == 0)
{
- if (!_syncingSelectedItems)
- {
- SelectedIndex = -1;
- }
+ SelectedIndex = -1;
}
foreach (var item in e.OldItems)
{
- MarkItemSelected(item, false);
+ var index = MarkItemSelected(item, false);
+ _selection.Remove(index);
}
removed = e.OldItems;
break;
+ case NotifyCollectionChangedAction.Replace:
+ throw new NotSupportedException("Replacing items in a SelectedItems collection is not supported.");
+
+ case NotifyCollectionChangedAction.Move:
+ throw new NotSupportedException("Moving items in a SelectedItems collection is not supported.");
+
case NotifyCollectionChangedAction.Reset:
- if (generator != null)
{
removed = new List();
+ added = new List();
- foreach (var item in generator.Containers)
+ foreach (var index in _selection.ToList())
{
- if (item?.ContainerControl != null)
+ var item = ElementAt(Items, index);
+
+ if (!SelectedItems.Contains(item))
{
- if (MarkContainerSelected(item.ContainerControl, false))
- {
- removed.Add(item.Item);
- }
+ MarkItemSelected(index, false);
+ removed.Add(item);
+ _selection.Remove(index);
}
}
- }
- if (SelectedItems.Count > 0)
- {
- _selectedItem = null;
- SelectedItemsAdded(SelectedItems);
- added = SelectedItems;
- }
- else if (!_syncingSelectedItems)
- {
- SelectedIndex = -1;
- }
-
- break;
-
- case NotifyCollectionChangedAction.Replace:
- foreach (var item in e.OldItems)
- {
- MarkItemSelected(item, false);
- }
-
- foreach (var item in e.NewItems)
- {
- MarkItemSelected(item, true);
+ Add(SelectedItems, added);
+ UpdateSelection();
}
- 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;
}
@@ -912,34 +909,6 @@ namespace Avalonia.Controls.Primitives
}
}
- ///
- /// Called when items are added to the collection.
- ///
- /// The added items.
- private void SelectedItemsAdded(IList items)
- {
- if (items.Count > 0)
- {
- foreach (var item in items)
- {
- MarkItemSelected(item, true);
- }
-
- if (SelectedItem == null && !_syncingSelectedItems)
- {
- var index = IndexOf(Items, items[0]);
-
- if (index != -1)
- {
- _selectedItem = items[0];
- _selectedIndex = index;
- RaisePropertyChanged(SelectedIndexProperty, -1, index, BindingPriority.LocalValue);
- RaisePropertyChanged(SelectedItemProperty, null, items[0], BindingPriority.LocalValue);
- }
- }
- }
- }
-
///
/// Subscribes to the CollectionChanged event, if any.
///
@@ -970,6 +939,112 @@ namespace Avalonia.Controls.Primitives
}
}
+ ///
+ /// Updates the selection due to a change to or
+ /// .
+ ///
+ /// The new selected index.
+ /// Whether to clear existing selection.
+ private void UpdateSelectedItem(int index, bool clear = true)
+ {
+ var oldIndex = _selectedIndex;
+ var oldItem = _selectedItem;
+
+ if (index == -1 && AlwaysSelected)
+ {
+ index = Math.Min(SelectedIndex, ItemCount - 1);
+ }
+
+ var item = ElementAt(Items, index);
+ var added = -1;
+ HashSet removed = null;
+
+ _selectedIndex = index;
+ _selectedItem = item;
+
+ if (oldIndex != index || _selection.HasMultiple)
+ {
+ if (clear)
+ {
+ removed = _selection.Clear();
+ }
+
+ if (index != -1)
+ {
+ if (_selection.Add(index))
+ {
+ added = index;
+ }
+
+ if (removed?.Contains(index) == true)
+ {
+ removed.Remove(index);
+ added = -1;
+ }
+ }
+
+ if (removed != null)
+ {
+ foreach (var i in removed)
+ {
+ MarkItemSelected(i, false);
+ }
+ }
+
+ MarkItemSelected(index, true);
+
+ RaisePropertyChanged(
+ SelectedIndexProperty,
+ oldIndex,
+ index);
+ }
+
+ if (!Equals(item, oldItem))
+ {
+ RaisePropertyChanged(
+ SelectedItemProperty,
+ oldItem,
+ item);
+ }
+
+ 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(),
+ removed?.Select(x => ElementAt(Items, x)).ToArray() ?? Array.Empty());
+ RaiseEvent(e);
+ }
+ }
+
+ private void UpdateSelectedItems(Action action)
+ {
+ try
+ {
+ _syncingSelectedItems = true;
+ action();
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(
+ LogArea.Property,
+ this,
+ "Error thrown updating SelectedItems: {Error}",
+ ex);
+ }
+ finally
+ {
+ _syncingSelectedItems = false;
+ }
+ }
+
private void UpdateFinished()
{
if (_updateSelectedIndex != int.MinValue)
@@ -981,5 +1056,104 @@ namespace Avalonia.Controls.Primitives
SelectedItems = _updateSelectedItems;
}
}
+
+ private class Selection : IEnumerable
+ {
+ private readonly List _list = new List();
+ private HashSet _set = new HashSet();
+
+ public bool HasItems => _set.Count > 0;
+ public bool HasMultiple => _set.Count > 1;
+
+ public bool Add(int index)
+ {
+ if (index == -1)
+ {
+ throw new ArgumentException("Invalid index", "index");
+ }
+
+ if (_set.Add(index))
+ {
+ _list.Add(index);
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool Remove(int index)
+ {
+ if (_set.Remove(index))
+ {
+ _list.RemoveAll(x => x == index);
+ return true;
+ }
+
+ return false;
+ }
+
+ public HashSet Clear()
+ {
+ var result = _set;
+ _list.Clear();
+ _set = new HashSet();
+ return result;
+ }
+
+ public void ItemsInserted(int index, int count)
+ {
+ _set = new HashSet();
+
+ for (var i = 0; i < _list.Count; ++i)
+ {
+ var ix = _list[i];
+
+ if (ix >= index)
+ {
+ var newIndex = ix + count;
+ _list[i] = newIndex;
+ _set.Add(newIndex);
+ }
+ else
+ {
+ _set.Add(ix);
+ }
+ }
+ }
+
+ public void ItemsRemoved(int index, int count)
+ {
+ var last = (index + count) - 1;
+
+ _set = new HashSet();
+
+ for (var i = 0; i < _list.Count; ++i)
+ {
+ var ix = _list[i];
+
+ if (ix >= index && ix <= last)
+ {
+ _list.RemoveAt(i--);
+ }
+ else if (ix > last)
+ {
+ var newIndex = ix - count;
+ _list[i] = newIndex;
+ _set.Add(newIndex);
+ }
+ else
+ {
+ _set.Add(ix);
+ }
+ }
+ }
+
+ public bool Contains(int index) => _set.Contains(index);
+
+ public int First() => HasItems ? _list[0] : -1;
+
+ public IEnumerator GetEnumerator() => _set.GetEnumerator();
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ }
}
}
diff --git a/src/Avalonia.Controls/RowDefinition.cs b/src/Avalonia.Controls/RowDefinition.cs
index ad7312d515..1f2f738670 100644
--- a/src/Avalonia.Controls/RowDefinition.cs
+++ b/src/Avalonia.Controls/RowDefinition.cs
@@ -29,7 +29,7 @@ namespace Avalonia.Controls
///
/// Initializes a new instance of the class.
///
- public RowDefinition()
+ public RowDefinition()
{
}
@@ -38,7 +38,7 @@ namespace Avalonia.Controls
///
/// The height of the row.
/// The height unit of the column.
- public RowDefinition(double value, GridUnitType type)
+ public RowDefinition(double value, GridUnitType type)
{
Height = new GridLength(value, type);
}
@@ -47,7 +47,7 @@ namespace Avalonia.Controls
/// Initializes a new instance of the class.
///
/// The height of the column.
- public RowDefinition(GridLength height)
+ public RowDefinition(GridLength height)
{
Height = height;
}
@@ -62,8 +62,15 @@ namespace Avalonia.Controls
///
public double MaxHeight
{
- get { return GetValue(MaxHeightProperty); }
- set { SetValue(MaxHeightProperty, value); }
+ get
+ {
+ return GetValue(MaxHeightProperty);
+ }
+ set
+ {
+ Parent?.InvalidateMeasure();
+ SetValue(MaxHeightProperty, value);
+ }
}
///
@@ -71,8 +78,15 @@ namespace Avalonia.Controls
///
public double MinHeight
{
- get { return GetValue(MinHeightProperty); }
- set { SetValue(MinHeightProperty, value); }
+ get
+ {
+ return GetValue(MinHeightProperty);
+ }
+ set
+ {
+ Parent?.InvalidateMeasure();
+ SetValue(MinHeightProperty, value);
+ }
}
///
@@ -80,12 +94,19 @@ namespace Avalonia.Controls
///
public GridLength Height
{
- get { return GetValue(HeightProperty); }
- set { SetValue(HeightProperty, value); }
+ get
+ {
+ return GetValue(HeightProperty);
+ }
+ set
+ {
+ Parent?.InvalidateMeasure();
+ SetValue(HeightProperty, value);
+ }
}
internal override GridLength UserSizeValueCache => this.Height;
internal override double UserMinSizeValueCache => this.MinHeight;
internal override double UserMaxSizeValueCache => this.MaxHeight;
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Controls/ShutdownMode.cs b/src/Avalonia.Controls/ShutdownMode.cs
index 46e27ff4e1..ee593c28a8 100644
--- a/src/Avalonia.Controls/ShutdownMode.cs
+++ b/src/Avalonia.Controls/ShutdownMode.cs
@@ -1,10 +1,12 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using Avalonia.Controls.ApplicationLifetimes;
+
namespace Avalonia.Controls
{
///
- /// Describes the possible values for .
+ /// Describes the possible values for .
///
public enum ShutdownMode
{
diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs
index c3fbce1d83..888f4a2013 100644
--- a/src/Avalonia.Controls/TreeView.cs
+++ b/src/Avalonia.Controls/TreeView.cs
@@ -409,7 +409,7 @@ namespace Avalonia.Controls
if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll))
{
- SelectingItemsControl.SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items);
+ SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items);
e.Handled = true;
}
}
@@ -521,7 +521,7 @@ namespace Avalonia.Controls
}
else if (multi && range)
{
- SelectingItemsControl.SynchronizeItems(
+ SynchronizeItems(
SelectedItems,
GetItemsInRange(selectedContainer as TreeViewItem, container as TreeViewItem));
}
@@ -778,5 +778,27 @@ namespace Avalonia.Controls
container.Classes.Set(":selected", selected);
}
}
+
+ ///
+ /// Makes a list of objects equal another (though doesn't preserve order).
+ ///
+ /// The items collection.
+ /// The desired items.
+ private static void SynchronizeItems(IList items, IEnumerable desired)
+ {
+ var list = items.Cast().ToList();
+ var toRemove = list.Except(desired).ToList();
+ var toAdd = desired.Except(list).ToList();
+
+ foreach (var i in toRemove)
+ {
+ items.Remove(i);
+ }
+
+ foreach (var i in toAdd)
+ {
+ items.Add(i);
+ }
+ }
}
}
diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs
index 0dab3e326a..3acf341c35 100644
--- a/src/Avalonia.Controls/WrapPanel.cs
+++ b/src/Avalonia.Controls/WrapPanel.cs
@@ -1,9 +1,7 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
+// This source file is adapted from the Windows Presentation Foundation project.
+// (https://github.com/dotnet/wpf/)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using Avalonia.Input;
using Avalonia.Layout;
diff --git a/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs b/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs
new file mode 100644
index 0000000000..a4f14a4138
--- /dev/null
+++ b/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
+{
+ ///
+ /// A ReactiveUI AutoSuspendHelper which initializes suspension hooks for
+ /// Avalonia applications. Call its constructor in your app's composition root,
+ /// before calling the RxApp.SuspensionHost.SetupDefaultSuspendResume method.
+ ///
+ public sealed class AutoSuspendHelper : IEnableLogger, IDisposable
+ {
+ private readonly Subject _shouldPersistState = new Subject();
+ private readonly Subject _isLaunchingNew = new Subject();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Pass in the Application.ApplicationLifetime property.
+ public AutoSuspendHelper(IApplicationLifetime lifetime)
+ {
+ RxApp.SuspensionHost.IsResuming = Observable.Never();
+ RxApp.SuspensionHost.IsLaunchingNew = _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();
+ AppDomain.CurrentDomain.UnhandledException += (o, e) => errored.OnNext(Unit.Default);
+ RxApp.SuspensionHost.ShouldInvalidateState = errored;
+ }
+
+ ///
+ /// Call this method in your App.OnFrameworkInitializationCompleted method.
+ ///
+ public void OnFrameworkInitializationCompleted() => _isLaunchingNew.OnNext(Unit.Default);
+
+ ///
+ /// Disposes internally stored observers.
+ ///
+ public void Dispose()
+ {
+ _shouldPersistState.Dispose();
+ _isLaunchingNew.Dispose();
+ }
+
+ private void OnControlledApplicationLifetimeExit()
+ {
+ this.Log().Debug("Received IControlledApplicationLifetime exit event.");
+ var manual = new ManualResetEvent(false);
+ _shouldPersistState.OnNext(Disposable.Create(() => manual.Set()));
+
+ manual.WaitOne();
+ this.Log().Debug("Completed actions on IControlledApplicationLifetime exit event.");
+ }
+ }
+}
diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs
index f7e063dfb5..146a4c75e7 100644
--- a/src/Avalonia.Styling/StyledElement.cs
+++ b/src/Avalonia.Styling/StyledElement.cs
@@ -392,6 +392,7 @@ namespace Avalonia
if (_initCount == 0 && !IsInitialized)
{
IsInitialized = true;
+ OnInitialized();
Initialized?.Invoke(this, EventArgs.Empty);
}
}
@@ -608,7 +609,14 @@ namespace Avalonia
protected virtual void OnDataContextEndUpdate()
{
}
-
+
+ ///
+ /// Called when the control finishes initialization.
+ ///
+ protected virtual void OnInitialized()
+ {
+ }
+
private static void DataContextNotifying(IAvaloniaObject o, bool updateStarted)
{
if (o is StyledElement element)
diff --git a/src/Avalonia.X11/X11KeyTransform.cs b/src/Avalonia.X11/X11KeyTransform.cs
index 26495111d1..c68cb04733 100644
--- a/src/Avalonia.X11/X11KeyTransform.cs
+++ b/src/Avalonia.X11/X11KeyTransform.cs
@@ -221,12 +221,8 @@ namespace Avalonia.X11
//{ X11Key.?, Key.DeadCharProcessed }
};
- public static Key ConvertKey(IntPtr key)
- {
- var ikey = key.ToInt32();
- Key result;
- return KeyDic.TryGetValue((X11Key)ikey, out result) ? result : Key.None;
- }
-}
+ public static Key ConvertKey(X11Key key)
+ => KeyDic.TryGetValue(key, out var result) ? result : Key.None;
+ }
}
diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs
index 01beebfff1..18c23aa31e 100644
--- a/src/Avalonia.X11/X11Window.cs
+++ b/src/Avalonia.X11/X11Window.cs
@@ -419,10 +419,21 @@ namespace Avalonia.X11
return;
var buffer = stackalloc byte[40];
- var latinKeysym = XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, 0);
+ var index = ev.KeyEvent.state.HasFlag(XModifierMask.ShiftMask);
+
+ // We need the latin key, since it's mainly used for hotkeys, we use a different API for text anyway
+ var key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 1 : 0).ToInt32();
+
+ // Manually switch the Shift index for the keypad,
+ // there should be a proper way to do this
+ if (ev.KeyEvent.state.HasFlag(XModifierMask.Mod2Mask)
+ && key > X11Key.Num_Lock && key <= X11Key.KP_9)
+ key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 0 : 1).ToInt32();
+
+
ScheduleInput(new RawKeyEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(),
ev.type == XEventName.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp,
- X11KeyTransform.ConvertKey(latinKeysym), TranslateModifiers(ev.KeyEvent.state)), ref ev);
+ X11KeyTransform.ConvertKey(key), TranslateModifiers(ev.KeyEvent.state)), ref ev);
if (ev.type == XEventName.KeyPress)
{
diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs
index eb2a8df2eb..19f92149ec 100644
--- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs
+++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs
@@ -64,14 +64,19 @@ namespace Avalonia.Data
object anchor = null,
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 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 ?
targetProperty?.GetMetadata(target.GetType()).DefaultBindingMode : Mode;
@@ -87,23 +92,19 @@ namespace Avalonia.Data
}
}
- private object ConvertValue(IList values, Type targetType)
+ private object ConvertValue(IList values, Type targetType, IMultiValueConverter converter)
{
var culture = CultureInfo.CurrentCulture;
- var converted = Converter.Convert(values, targetType, ConverterParameter, culture);
+ var converted = converter.Convert(values, targetType, ConverterParameter, culture);
- if (converted == AvaloniaProperty.UnsetValue && FallbackValue != null)
+ if (converted == BindingOperations.DoNothing)
{
- converted = FallbackValue;
+ return converted;
}
- // 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)))
+ if (converted == AvaloniaProperty.UnsetValue)
{
- converted = string.Format(culture, StringFormat, converted);
+ converted = FallbackValue;
}
return converted;
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs
index bbeba2fe2e..bc5aa1846a 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs
@@ -9,6 +9,7 @@ using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.Markup.Data;
using Avalonia.Styling;
+using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests.Primitives
@@ -23,6 +24,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Minimum = 100,
Maximum = 50,
};
+ var root = new TestRoot(target);
Assert.Equal(100, target.Minimum);
Assert.Equal(100, target.Maximum);
@@ -37,6 +39,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Maximum = 50,
Value = 100,
};
+ var root = new TestRoot(target);
Assert.Equal(0, target.Minimum);
Assert.Equal(50, target.Maximum);
@@ -52,6 +55,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Maximum = 100,
Value = 50,
};
+ var root = new TestRoot(target);
target.Minimum = 200;
@@ -69,6 +73,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Maximum = 100,
Value = 100,
};
+ var root = new TestRoot(target);
target.Maximum = 50;
@@ -161,6 +166,38 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(expected, track.Value);
}
+ [Fact]
+ public void Coercion_Should_Not_Be_Done_During_Initialization()
+ {
+ var target = new TestRange();
+
+ target.BeginInit();
+
+ var root = new TestRoot(target);
+ target.Minimum = 1;
+ Assert.Equal(0, target.Value);
+
+ target.Value = 50;
+ target.EndInit();
+
+ Assert.Equal(50, target.Value);
+ }
+
+ [Fact]
+ public void Coercion_Should_Be_Done_After_Initialization()
+ {
+ var target = new TestRange();
+
+ target.BeginInit();
+
+ var root = new TestRoot(target);
+ target.Minimum = 1;
+
+ target.EndInit();
+
+ Assert.Equal(1, target.Value);
+ }
+
private class TestRange : RangeBase
{
}
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
index 8e421bf0a2..75cababf54 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
@@ -536,6 +536,9 @@ namespace Avalonia.Controls.UnitTests.Primitives
SelectedIndex = 1,
};
+ target.ApplyTemplate();
+ target.Presenter.ApplyTemplate();
+
var called = false;
target.SelectionChanged += (s, e) =>
@@ -545,8 +548,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
called = true;
};
- target.ApplyTemplate();
- target.Presenter.ApplyTemplate();
target.SelectedIndex = -1;
Assert.True(called);
@@ -783,6 +784,116 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(2, vm.Child.SelectedIndex);
}
+ [Fact]
+ public void Should_Select_Correct_Item_When_Duplicate_Items_Are_Present()
+ {
+ var target = new ListBox
+ {
+ Template = Template(),
+ Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz"},
+ };
+
+ target.ApplyTemplate();
+ target.Presenter.ApplyTemplate();
+ _helper.Down((Interactive)target.Presenter.Panel.Children[3]);
+
+ 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
+ {
+ "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
+ {
+ "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
+ {
+ "Foo",
+ "Bar",
+ "Baz"
+ };
+
+ var target = new ListBox
+ {
+ Template = Template(),
+ Items = items,
+ SelectedIndex = 1,
+ };
+
+ target.ApplyTemplate();
+ target.Presenter.ApplyTemplate();
+
+ items[1] = "Qux";
+
+ Assert.Equal(1, target.SelectedIndex);
+ Assert.Equal("Qux", target.SelectedItem);
+ }
+
private FuncControlTemplate Template()
{
return new FuncControlTemplate(control =>
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
index 9ef7d567c8..a33d97779e 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
@@ -4,12 +4,15 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Interactivity;
using Avalonia.Markup.Data;
using Xunit;
@@ -17,6 +20,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
public class SelectingItemsControlTests_Multiple
{
+ private MouseTestHelper _helper = new MouseTestHelper();
+
[Fact]
public void Setting_SelectedIndex_Should_Add_To_SelectedItems()
{
@@ -258,31 +263,25 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
[Fact]
- public void Replacing_First_SelectedItem_Should_Update_SelectedItem_SelectedIndex()
+ public void Setting_SelectedIndex_Should_Unmark_Previously_Selected_Containers()
{
- var items = new[]
- {
- new ListBoxItem(),
- new ListBoxItem(),
- new ListBoxItem(),
- };
-
var target = new TestSelector
{
- Items = items,
+ Items = new[] { "foo", "bar", "baz" },
Template = Template(),
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
- target.SelectedIndex = 1;
- target.SelectedItems[0] = items[2];
- Assert.Equal(2, target.SelectedIndex);
- Assert.Equal(items[2], target.SelectedItem);
- Assert.False(items[0].IsSelected);
- Assert.False(items[1].IsSelected);
- Assert.True(items[2].IsSelected);
+ target.SelectedItems.Add("foo");
+ target.SelectedItems.Add("bar");
+
+ Assert.Equal(new[] { 0, 1 }, SelectedContainers(target));
+
+ target.SelectedIndex = 2;
+
+ Assert.Equal(new[] { 2 }, SelectedContainers(target));
}
[Fact]
@@ -361,6 +360,52 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(new[] { "baz", "qux", "qiz" }, target.SelectedItems.Cast().ToList());
}
+ [Fact]
+ public void Setting_SelectedIndex_After_Range_Should_Unmark_Previously_Selected_Containers()
+ {
+ var target = new TestSelector
+ {
+ Items = new[] { "foo", "bar", "baz", "qux" },
+ Template = Template(),
+ SelectedIndex = 0,
+ SelectionMode = SelectionMode.Multiple,
+ };
+
+ target.ApplyTemplate();
+ target.Presenter.ApplyTemplate();
+
+ target.SelectRange(2);
+
+ Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target));
+
+ target.SelectedIndex = 3;
+
+ Assert.Equal(new[] { 3 }, SelectedContainers(target));
+ }
+
+ [Fact]
+ public void Toggling_Selection_After_Range_Should_Work()
+ {
+ var target = new TestSelector
+ {
+ Items = new[] { "foo", "bar", "baz", "foo", "bar", "baz" },
+ Template = Template(),
+ SelectedIndex = 0,
+ SelectionMode = SelectionMode.Multiple,
+ };
+
+ target.ApplyTemplate();
+ target.Presenter.ApplyTemplate();
+
+ target.SelectRange(3);
+
+ Assert.Equal(new[] { 0, 1, 2, 3 }, SelectedContainers(target));
+
+ target.Toggle(4);
+
+ Assert.Equal(new[] { 0, 1, 2, 3, 4 }, SelectedContainers(target));
+ }
+
[Fact]
public void Suprious_SelectedIndex_Changes_Should_Not_Be_Triggered()
{
@@ -382,6 +427,40 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(new[] { -1, 1, 0 }, selectedIndexes);
}
+ [Fact]
+ public void Can_Set_SelectedIndex_To_Another_Selected_Item()
+ {
+ var target = new TestSelector
+ {
+ Items = new[] { "foo", "bar", "baz" },
+ Template = Template(),
+ };
+
+ target.ApplyTemplate();
+ target.Presenter.ApplyTemplate();
+ target.SelectedItems.Add("foo");
+ target.SelectedItems.Add("bar");
+
+ Assert.Equal(0, target.SelectedIndex);
+ Assert.Equal(new[] { "foo", "bar" }, target.SelectedItems);
+ Assert.Equal(new[] { 0, 1 }, SelectedContainers(target));
+
+ var raised = false;
+ target.SelectionChanged += (s, e) =>
+ {
+ raised = true;
+ Assert.Empty(e.AddedItems);
+ Assert.Equal(new[] { "foo" }, e.RemovedItems);
+ };
+
+ target.SelectedIndex = 1;
+
+ Assert.True(raised);
+ Assert.Equal(1, target.SelectedIndex);
+ Assert.Equal(new[] { "bar" }, target.SelectedItems);
+ Assert.Equal(new[] { 1 }, SelectedContainers(target));
+ }
+
///
/// Tests a problem discovered with ListBox with selection.
///
@@ -471,6 +550,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
DataContext = items,
Template = Template(),
+ Items = items,
};
var called = false;
@@ -540,35 +620,418 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.True(called);
}
+
+ [Fact]
+ public void Shift_Selecting_From_No_Selection_Selects_From_Start()
+ {
+ var target = new ListBox
+ {
+ Template = Template(),
+ Items = new[] { "Foo", "Bar", "Baz" },
+ SelectionMode = SelectionMode.Multiple,
+ };
+
+ target.ApplyTemplate();
+ target.Presenter.ApplyTemplate();
+ _helper.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]
- 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
{
+ 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
+ {
+ "Foo",
+ "Bar",
+ "Baz"
+ };
+
+ var target = new ListBox
+ {
+ Template = Template(),
Items = items,
+ SelectionMode = SelectionMode.Multiple,
+ };
+
+ target.ApplyTemplate();
+ target.Presenter.ApplyTemplate();
+
+ target.SelectAll();
+ items.Insert(0, "Qux");
+
+ Assert.Equal(1, target.SelectedIndex);
+ Assert.Equal("Foo", target.SelectedItem);
+ Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems);
+ Assert.Equal(new[] { 1, 2, 3 }, SelectedContainers(target));
+ }
+
+ [Fact]
+ public void Removing_Item_Before_SelectedItem_Should_Update_Selection()
+ {
+ var items = new ObservableCollection
+ {
+ "Foo",
+ "Bar",
+ "Baz"
+ };
+
+ var target = new TestSelector
+ {
Template = Template(),
- 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
{
- Assert.Equal(new[] { "foo",}, e.AddedItems.Cast());
- Assert.Equal(new[] { "bar" }, e.RemovedItems.Cast());
- called = true;
+ "Foo",
+ "Bar",
+ "Baz"
+ };
+
+ var target = new ListBox
+ {
+ Template = Template(),
+ Items = items,
+ SelectionMode = SelectionMode.Multiple,
};
target.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
+ {
+ "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(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(x => new TextBlock { Width = 20, Height = 10 }),
+ SelectionMode = SelectionMode.Multiple,
+ };
+
+ target.ApplyTemplate();
+ target.Presenter.ApplyTemplate();
+ target.SelectAll();
+
+ Assert.Equal(3, target.SelectedItems.Count);
+
+ _helper.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Right);
+
+ Assert.Equal(3, target.SelectedItems.Count);
+ }
+
+ [Fact]
+ public void Right_Click_On_UnselectedItem_Should_Clear_Existing_Selection()
+ {
+ var target = new ListBox
+ {
+ Template = Template(),
+ Items = new[] { "Foo", "Bar", "Baz" },
+ ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }),
+ SelectionMode = SelectionMode.Multiple,
+ };
+
+ target.ApplyTemplate();
+ target.Presenter.ApplyTemplate();
+ _helper.Click((Interactive)target.Presenter.Panel.Children[0]);
+ _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: InputModifiers.Shift);
+
+ Assert.Equal(2, target.SelectedItems.Count);
+
+ _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right);
+
+ Assert.Equal(1, target.SelectedItems.Count);
+ }
+
+ private IEnumerable SelectedContainers(SelectingItemsControl target)
+ {
+ return target.Presenter.Panel.Children
+ .Select((x, i) => x.Classes.Contains(":selected") ? i : -1)
+ .Where(x => x != -1);
+ }
private FuncControlTemplate Template()
{
@@ -598,10 +1061,10 @@ namespace Avalonia.Controls.UnitTests.Primitives
set { base.SelectionMode = value; }
}
- public void SelectRange(int index)
- {
- UpdateSelection(index, true, true);
- }
+ public new void SelectAll() => base.SelectAll();
+ public new void UnselectAll() => base.UnselectAll();
+ public void SelectRange(int index) => UpdateSelection(index, true, true);
+ public void Toggle(int index) => UpdateSelection(index, true, false, true);
}
private class OldDataContextViewModel
diff --git a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests_Converters.cs b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests_Converters.cs
index bd4b5b9d04..d3e3ce5507 100644
--- a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests_Converters.cs
+++ b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests_Converters.cs
@@ -5,11 +5,10 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
-using System.Text;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Data.Converters;
-using Avalonia.Data.Core;
+using Avalonia.Layout;
using Xunit;
namespace Avalonia.Markup.UnitTests.Data
@@ -21,7 +20,30 @@ namespace Avalonia.Markup.UnitTests.Data
{
var textBlock = new TextBlock
{
- DataContext = new MultiBindingTests_Converters.Class1(),
+ DataContext = new Class1(),
+ };
+
+ var target = new MultiBinding
+ {
+ StringFormat = "{0:0.0} + {1:00}",
+ Bindings =
+ {
+ new Binding(nameof(Class1.Foo)),
+ new Binding(nameof(Class1.Bar)),
+ }
+ };
+
+ textBlock.Bind(TextBlock.TextProperty, target);
+
+ Assert.Equal("1.0 + 02", textBlock.Text);
+ }
+
+ [Fact]
+ public void StringFormat_Should_Be_Applied_After_Converter()
+ {
+ var textBlock = new TextBlock
+ {
+ DataContext = new Class1(),
};
var target = new MultiBinding
@@ -30,8 +52,8 @@ namespace Avalonia.Markup.UnitTests.Data
Converter = new SumOfDoublesConverter(),
Bindings =
{
- new Binding(nameof(MultiBindingTests_Converters.Class1.Foo)),
- new Binding(nameof(MultiBindingTests_Converters.Class1.Bar)),
+ new Binding(nameof(Class1.Foo)),
+ new Binding(nameof(Class1.Bar)),
}
};
@@ -45,7 +67,7 @@ namespace Avalonia.Markup.UnitTests.Data
{
var textBlock = new TextBlock
{
- DataContext = new MultiBindingTests_Converters.Class1(),
+ DataContext = new Class1(),
};
var target = new MultiBinding
@@ -54,12 +76,12 @@ namespace Avalonia.Markup.UnitTests.Data
Converter = new SumOfDoublesConverter(),
Bindings =
{
- new Binding(nameof(MultiBindingTests_Converters.Class1.Foo)),
- new Binding(nameof(MultiBindingTests_Converters.Class1.Bar)),
+ new Binding(nameof(Class1.Foo)),
+ new Binding(nameof(Class1.Bar)),
}
};
- textBlock.Bind(TextBlock.WidthProperty, target);
+ textBlock.Bind(Layoutable.WidthProperty, target);
Assert.Equal(3.0, textBlock.Width);
}
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ValueConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ValueConverterTests.cs
new file mode 100644
index 0000000000..6f2c4363e2
--- /dev/null
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ValueConverterTests.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Globalization;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Data.Converters;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Markup.Xaml.UnitTests.Converters
+{
+ public class ValueConverterTests
+ {
+ [Fact]
+ public void ValueConverter_Special_Values_Work()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var xaml = @"
+
+
+ ";
+ var loader = new AvaloniaXamlLoader();
+ var window = (Window)loader.Load(xaml);
+ var textBlock = window.FindControl("textBlock");
+
+ window.ApplyTemplate();
+
+ window.DataContext = 2;
+ Assert.Equal("foo", textBlock.Text);
+
+ window.DataContext = -3;
+ Assert.Equal("foo", textBlock.Text);
+
+ window.DataContext = 0;
+ Assert.Equal("bar", textBlock.Text);
+ }
+ }
+ }
+
+ public class TestConverter : IValueConverter
+ {
+ public static readonly TestConverter Instance = new TestConverter();
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is int i)
+ {
+ if (i > 0)
+ {
+ return "foo";
+ }
+
+ if (i == 0)
+ {
+ return AvaloniaProperty.UnsetValue;
+ }
+
+ return BindingOperations.DoNothing;
+ }
+
+ return "(default)";
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs
index 0a00b9d39d..e412657711 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs
@@ -1,20 +1,10 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Data;
-using Avalonia.Markup.Data;
-using Moq;
-using Xunit;
-using System.ComponentModel;
-using System.Runtime.CompilerServices;
using Avalonia.UnitTests;
-using Avalonia.Data.Converters;
-using Avalonia.Data.Core;
+using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Data
{
@@ -40,5 +30,32 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
Assert.Equal("foo", textBlock.Text);
}
}
+
+ [Fact]
+ public void Binding_To_DoNothing_Works()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var xaml = @"
+
+
+ ";
+ var loader = new AvaloniaXamlLoader();
+ var window = (Window)loader.Load(xaml);
+ var textBlock = window.FindControl("textBlock");
+
+ window.ApplyTemplate();
+
+ window.DataContext = "foo";
+ Assert.Equal("foo", textBlock.Text);
+
+ window.DataContext = BindingOperations.DoNothing;
+ Assert.Equal("foo", textBlock.Text);
+
+ window.DataContext = "bar";
+ Assert.Equal("bar", textBlock.Text);
+ }
+ }
}
}
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
index 7fe0fc4a08..720ff3b0de 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
@@ -907,6 +907,25 @@ do we need it?")]
}
}
+ [Fact]
+ public void Slider_Properties_Can_Be_Set_In_Any_Order()
+ {
+ using (UnitTestApplication.Start(TestServices.MockWindowingPlatform))
+ {
+ var xaml = @"
+
+
+ ";
+
+ var window = AvaloniaXamlLoader.Parse(xaml);
+ var slider = (Slider)window.Content;
+
+ Assert.Equal(0, slider.Minimum);
+ Assert.Equal(1000, slider.Maximum);
+ Assert.Equal(500, slider.Value);
+ }
+ }
+
private class SelectedItemsViewModel : INotifyPropertyChanged
{
public string[] Items { get; set; }
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
index 14abebcdb5..3930608515 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
@@ -331,6 +331,35 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
+ [Fact(Skip="Issue #2592")]
+ public void MultiBinding_To_TextBlock_Text_With_StringConverter_Works()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var xaml = @"
+
+
+
+
+
+
+
+
+
+ ";
+ var loader = new AvaloniaXamlLoader();
+ var window = (Window)loader.Load(xaml);
+ var textBlock = window.FindControl("textBlock");
+
+ textBlock.DataContext = new WindowViewModel();
+ window.ApplyTemplate();
+
+ Assert.Equal("Hello World!", textBlock.Text);
+ }
+ }
+
[Fact]
public void Binding_OneWayToSource_Works()
{
@@ -356,6 +385,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
private class WindowViewModel
{
public bool ShowInTaskbar { get; set; }
+ public string Greeting1 { get; set; } = "Hello";
+ public string Greeting2 { get; set; } = "World";
}
}
}
diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs
new file mode 100644
index 0000000000..876f37cc9e
--- /dev/null
+++ b/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.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.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().Example);
+ }
+ }
+
+ [Fact]
+ public void AutoSuspendHelper_Should_Throw_For_Not_Supported_Lifetimes()
+ {
+ using (UnitTestApplication.Start(TestServices.MockWindowingPlatform))
+ using (var lifetime = new ExoticApplicationLifetimeWithoutLifecycleEvents())
+ {
+ var application = AvaloniaLocator.Current.GetService();
+ application.ApplicationLifetime = lifetime;
+ Assert.Throws(() => new AutoSuspendHelper(application.ApplicationLifetime));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs
index 2c81e8fea3..1d85312b1a 100644
--- a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs
+++ b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs
@@ -171,7 +171,7 @@ namespace Avalonia.ReactiveUI.UnitTests
[Fact]
public void Activation_For_View_Fetcher_Should_Support_Windows()
{
- using (var application = UnitTestApplication.Start(TestServices.MockWindowingPlatform))
+ using (UnitTestApplication.Start(TestServices.MockWindowingPlatform))
{
var window = new TestWindowWithWhenActivated();
Assert.False(window.Active);
@@ -187,7 +187,7 @@ namespace Avalonia.ReactiveUI.UnitTests
[Fact]
public void Activatable_Window_View_Model_Is_Activated_And_Deactivated()
{
- using (var application = UnitTestApplication.Start(TestServices.MockWindowingPlatform))
+ using (UnitTestApplication.Start(TestServices.MockWindowingPlatform))
{
var viewModel = new ActivatableViewModel();
var window = new ActivatableWindow { ViewModel = viewModel };