diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index d59c15bb6a..c7a75f5a70 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -1,10 +1,7 @@ - - - - + - + + + + + diff --git a/samples/ControlCatalog/Pages/ButtonPage.xaml.cs b/samples/ControlCatalog/Pages/ButtonPage.xaml.cs index 1d0c228a0e..5e555c8c91 100644 --- a/samples/ControlCatalog/Pages/ButtonPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ButtonPage.xaml.cs @@ -5,5 +5,25 @@ namespace ControlCatalog.Pages { public class ButtonPage : UserControl { + private int repeatButtonClickCount = 0; + + public ButtonPage() + { + InitializeComponent(); + + this.FindControl("RepeatButton").Click += OnRepeatButtonClick; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public void OnRepeatButtonClick(object sender, object args) + { + repeatButtonClickCount++; + var textBlock = this.FindControl("RepeatButtonTextBlock"); + textBlock.Text = $"Repeat Button: {repeatButtonClickCount}"; + } } } diff --git a/samples/ControlCatalog/Pages/DatePickerPage.xaml b/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml similarity index 75% rename from samples/ControlCatalog/Pages/DatePickerPage.xaml rename to samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml index 30d5a7506f..107472105a 100644 --- a/samples/ControlCatalog/Pages/DatePickerPage.xaml +++ b/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml @@ -1,8 +1,8 @@ + x:Class="ControlCatalog.Pages.CalendarDatePickerPage"> - DatePicker + CalendarDatePicker A control for selecting dates with a calendar drop-down - - - - - - - + diff --git a/samples/ControlCatalog/Pages/DatePickerPage.xaml.cs b/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml.cs similarity index 57% rename from samples/ControlCatalog/Pages/DatePickerPage.xaml.cs rename to samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml.cs index ef01887c9e..95bdeb363a 100644 --- a/samples/ControlCatalog/Pages/DatePickerPage.xaml.cs +++ b/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml.cs @@ -4,17 +4,17 @@ using System; namespace ControlCatalog.Pages { - public class DatePickerPage : UserControl + public class CalendarDatePickerPage : UserControl { - public DatePickerPage() + public CalendarDatePickerPage() { InitializeComponent(); - var dp1 = this.FindControl("DatePicker1"); - var dp2 = this.FindControl("DatePicker2"); - var dp3 = this.FindControl("DatePicker3"); - var dp4 = this.FindControl("DatePicker4"); - var dp5 = this.FindControl("DatePicker5"); + var dp1 = this.FindControl("DatePicker1"); + var dp2 = this.FindControl("DatePicker2"); + var dp3 = this.FindControl("DatePicker3"); + var dp4 = this.FindControl("DatePicker4"); + var dp5 = this.FindControl("DatePicker5"); dp1.SelectedDate = DateTime.Today; dp2.SelectedDate = DateTime.Today.AddDays(10); diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml b/samples/ControlCatalog/Pages/TreeViewPage.xaml index 6019d5f91f..789b45e62c 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml @@ -20,6 +20,7 @@ + Single diff --git a/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs index d396ef2b3d..5bc23e2fe5 100644 --- a/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs @@ -23,12 +23,14 @@ namespace ControlCatalog.ViewModels AddItemCommand = ReactiveCommand.Create(AddItem); RemoveItemCommand = ReactiveCommand.Create(RemoveItem); + SelectRandomItemCommand = ReactiveCommand.Create(SelectRandomItem); } public ObservableCollection Items { get; } public SelectionModel Selection { get; } public ReactiveCommand AddItemCommand { get; } public ReactiveCommand RemoveItemCommand { get; } + public ReactiveCommand SelectRandomItemCommand { get; } public SelectionMode SelectionMode { @@ -74,6 +76,15 @@ namespace ControlCatalog.ViewModels } } + private void SelectRandomItem() + { + var random = new Random(); + var depth = random.Next(4); + var indexes = Enumerable.Range(0, 4).Select(x => random.Next(10)); + var path = new IndexPath(indexes); + Selection.SelectedIndex = path; + } + private void SelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) { var selected = string.Join(",", e.SelectedIndices); diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index 84ef0fb695..8fc2a7b77c 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -12,7 +12,7 @@ namespace Avalonia.Data.Core.Plugins public class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin { /// - public bool Match(object obj, string propertyName) => true; + public bool Match(object obj, string propertyName) => GetPropertyWithName(obj.GetType(), propertyName) != null; /// /// Starts monitoring the value of a property on an object. @@ -30,10 +30,7 @@ namespace Avalonia.Data.Core.Plugins reference.TryGetTarget(out object instance); - const BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | - BindingFlags.Static | BindingFlags.Instance; - - var p = instance.GetType().GetProperty(propertyName, bindingFlags); + var p = GetPropertyWithName(instance.GetType(), propertyName); if (p != null) { @@ -47,6 +44,14 @@ namespace Avalonia.Data.Core.Plugins } } + private static PropertyInfo GetPropertyWithName(Type type, string propertyName) + { + const BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | + BindingFlags.Static | BindingFlags.Instance; + + return type.GetProperty(propertyName, bindingFlags); + } + private class Accessor : PropertyAccessorBase, IWeakSubscriber { private readonly WeakReference _reference; diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index e20685b1dd..a0868152f6 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -62,8 +62,13 @@ namespace Avalonia.Data.Core if (accessor == null) { - throw new NotSupportedException( - $"Could not find a matching property accessor for {PropertyName}."); + reference.TryGetTarget(out object instance); + + var message = $"Could not find a matching property accessor for '{PropertyName}' on '{instance}'"; + + var exception = new MissingMemberException(message); + + accessor = new PropertyError(new BindingNotification(exception, BindingErrorType.Error)); } _accessor = accessor; diff --git a/src/Avalonia.Base/Properties/AssemblyInfo.cs b/src/Avalonia.Base/Properties/AssemblyInfo.cs index 75d58f45d5..0664f22dcb 100644 --- a/src/Avalonia.Base/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Base/Properties/AssemblyInfo.cs @@ -7,4 +7,4 @@ using Avalonia.Metadata; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Data.Converters")] [assembly: InternalsVisibleTo("Avalonia.Base.UnitTests")] [assembly: InternalsVisibleTo("Avalonia.UnitTests")] -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index a1a2d6c3d0..7949a62949 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -8,6 +8,11 @@ namespace Avalonia.Utilities /// public static class MathUtilities { + // smallest such that 1.0+DoubleEpsilon != 1.0 + private const double DoubleEpsilon = 2.2204460492503131e-016; + + private const float FloatEpsilon = 1.192092896e-07F; + /// /// AreClose - Returns whether or not two doubles are "close". That is, whether or /// not they are within epsilon of each other. @@ -18,11 +23,26 @@ namespace Avalonia.Utilities { //in case they are Infinities (then epsilon check does not work) if (value1 == value2) return true; - double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * double.Epsilon; + double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * DoubleEpsilon; double delta = value1 - value2; return (-eps < delta) && (eps > delta); } + /// + /// AreClose - Returns whether or not two floats are "close". That is, whether or + /// not they are within epsilon of each other. + /// + /// The first float to compare. + /// The second float to compare. + public static bool AreClose(float value1, float value2) + { + //in case they are Infinities (then epsilon check does not work) + if (value1 == value2) return true; + float eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0f) * FloatEpsilon; + float delta = value1 - value2; + return (-eps < delta) && (eps > delta); + } + /// /// LessThan - Returns whether or not the first double is less than the second double. /// That is, whether or not the first is strictly less than *and* not within epsilon of @@ -35,6 +55,18 @@ namespace Avalonia.Utilities return (value1 < value2) && !AreClose(value1, value2); } + /// + /// LessThan - Returns whether or not the first float is less than the second float. + /// That is, whether or not the first is strictly less than *and* not within epsilon of + /// the other number. + /// + /// The first single float to compare. + /// The second single float to compare. + public static bool LessThan(float value1, float value2) + { + return (value1 < value2) && !AreClose(value1, value2); + } + /// /// GreaterThan - Returns whether or not the first double is greater than the second double. /// That is, whether or not the first is strictly greater than *and* not within epsilon of @@ -47,6 +79,18 @@ namespace Avalonia.Utilities return (value1 > value2) && !AreClose(value1, value2); } + /// + /// GreaterThan - Returns whether or not the first float is greater than the second float. + /// That is, whether or not the first is strictly greater than *and* not within epsilon of + /// the other number. + /// + /// The first float to compare. + /// The second float to compare. + public static bool GreaterThan(float value1, float value2) + { + return (value1 > value2) && !AreClose(value1, value2); + } + /// /// LessThanOrClose - Returns whether or not the first double is less than or close to /// the second double. That is, whether or not the first is strictly less than or within @@ -59,6 +103,18 @@ namespace Avalonia.Utilities return (value1 < value2) || AreClose(value1, value2); } + /// + /// LessThanOrClose - Returns whether or not the first float is less than or close to + /// the second float. That is, whether or not the first is strictly less than or within + /// epsilon of the other number. + /// + /// The first float to compare. + /// The second float to compare. + public static bool LessThanOrClose(float value1, float value2) + { + return (value1 < value2) || AreClose(value1, value2); + } + /// /// GreaterThanOrClose - Returns whether or not the first double is greater than or close to /// the second double. That is, whether or not the first is strictly greater than or within @@ -71,6 +127,18 @@ namespace Avalonia.Utilities return (value1 > value2) || AreClose(value1, value2); } + /// + /// GreaterThanOrClose - Returns whether or not the first float is greater than or close to + /// the second float. That is, whether or not the first is strictly greater than or within + /// epsilon of the other number. + /// + /// The first float to compare. + /// The second float to compare. + public static bool GreaterThanOrClose(float value1, float value2) + { + return (value1 > value2) || AreClose(value1, value2); + } + /// /// IsOne - Returns whether or not the double is "close" to 1. Same as AreClose(double, 1), /// but this is faster. @@ -78,7 +146,17 @@ namespace Avalonia.Utilities /// The double to compare to 1. public static bool IsOne(double value) { - return Math.Abs(value - 1.0) < 10.0 * double.Epsilon; + return Math.Abs(value - 1.0) < 10.0 * DoubleEpsilon; + } + + /// + /// IsOne - Returns whether or not the float is "close" to 1. Same as AreClose(float, 1), + /// but this is faster. + /// + /// The float to compare to 1. + public static bool IsOne(float value) + { + return Math.Abs(value - 1.0f) < 10.0f * FloatEpsilon; } /// @@ -88,7 +166,17 @@ namespace Avalonia.Utilities /// The double to compare to 0. public static bool IsZero(double value) { - return Math.Abs(value) < 10.0 * double.Epsilon; + return Math.Abs(value) < 10.0 * DoubleEpsilon; + } + + /// + /// IsZero - Returns whether or not the float is "close" to 0. Same as AreClose(float, 0), + /// but this is faster. + /// + /// The float to compare to 0. + public static bool IsZero(float value) + { + return Math.Abs(value) < 10.0f * FloatEpsilon; } /// diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index b38cc56a17..31101dc0f1 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -468,10 +468,11 @@ namespace Avalonia.Controls /// /// dependency property. public static readonly DirectProperty TextProperty = - AvaloniaProperty.RegisterDirect( - nameof(Text), + TextBlock.TextProperty.AddOwnerWithDataValidation( o => o.Text, - (o, v) => o.Text = v); + (o, v) => o.Text = v, + defaultBindingMode: BindingMode.TwoWay, + enableDataValidation: true); /// /// Identifies the @@ -1244,6 +1245,20 @@ namespace Avalonia.Controls base.OnApplyTemplate(e); } + + /// + /// Called to update the validation state for properties for which data validation is + /// enabled. + /// + /// The property. + /// The new binding value for the property. + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + { + if (property == TextProperty) + { + DataValidationErrors.SetError(this, value.Error); + } + } /// /// Provides handling for the diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index 53c6a54b4d..4cf7db74d9 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -998,10 +998,10 @@ namespace Avalonia.Controls /// - /// Gets or sets a value indicating whether DatePicker should change its + /// Gets or sets a value indicating whether CalendarDatePicker should change its /// DisplayDate because of a SelectedDate change on its Calendar. /// - internal bool DatePickerDisplayDateFlag { get; set; } + internal bool CalendarDatePickerDisplayDateFlag { get; set; } internal CalendarDayButton FindDayButtonFromDay(DateTime day) { diff --git a/src/Avalonia.Controls/Calendar/DatePicker.cs b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs similarity index 87% rename from src/Avalonia.Controls/Calendar/DatePicker.cs rename to src/Avalonia.Controls/Calendar/CalendarDatePicker.cs index 0f53dc1364..b987f065be 100644 --- a/src/Avalonia.Controls/Calendar/DatePicker.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs @@ -16,29 +16,29 @@ namespace Avalonia.Controls { /// /// Provides data for the - /// + /// /// event. /// - public class DatePickerDateValidationErrorEventArgs : EventArgs + public class CalendarDatePickerDateValidationErrorEventArgs : EventArgs { private bool _throwException; /// /// Initializes a new instance of the - /// + /// /// class. /// /// /// The initial exception from the - /// + /// /// event. /// /// /// The text that caused the - /// + /// /// event. /// - public DatePickerDateValidationErrorEventArgs(Exception exception, string text) + public CalendarDatePickerDateValidationErrorEventArgs(Exception exception, string text) { this.Text = text; this.Exception = exception; @@ -46,7 +46,7 @@ namespace Avalonia.Controls /// /// Gets the initial exception associated with the - /// + /// /// event. /// /// @@ -56,7 +56,7 @@ namespace Avalonia.Controls /// /// Gets the text that caused the - /// + /// /// event. /// /// @@ -66,7 +66,7 @@ namespace Avalonia.Controls /// /// Gets or sets a value indicating whether - /// + /// /// should be thrown. /// /// @@ -74,7 +74,7 @@ namespace Avalonia.Controls /// /// /// If set to true and - /// + /// /// is null. /// public bool ThrowException @@ -93,9 +93,9 @@ namespace Avalonia.Controls /// /// Specifies date formats for a - /// . + /// . /// - public enum DatePickerFormat + public enum CalendarDatePickerFormat { /// /// Specifies that the date should be displayed using unabbreviated days @@ -115,7 +115,7 @@ namespace Avalonia.Controls Custom = 2 } - public class DatePicker : TemplatedControl + public class CalendarDatePicker : TemplatedControl { private const string ElementTextBox = "PART_TextBox"; private const string ElementButton = "PART_Button"; @@ -154,59 +154,59 @@ namespace Avalonia.Controls /// public CalendarBlackoutDatesCollection BlackoutDates { get; private set; } - public static readonly DirectProperty DisplayDateProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty DisplayDateProperty = + AvaloniaProperty.RegisterDirect( nameof(DisplayDate), o => o.DisplayDate, (o, v) => o.DisplayDate = v); - public static readonly DirectProperty DisplayDateStartProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty DisplayDateStartProperty = + AvaloniaProperty.RegisterDirect( nameof(DisplayDateStart), o => o.DisplayDateStart, (o, v) => o.DisplayDateStart = v); - public static readonly DirectProperty DisplayDateEndProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty DisplayDateEndProperty = + AvaloniaProperty.RegisterDirect( nameof(DisplayDateEnd), o => o.DisplayDateEnd, (o, v) => o.DisplayDateEnd = v); public static readonly StyledProperty FirstDayOfWeekProperty = - AvaloniaProperty.Register(nameof(FirstDayOfWeek)); + AvaloniaProperty.Register(nameof(FirstDayOfWeek)); - public static readonly DirectProperty IsDropDownOpenProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty IsDropDownOpenProperty = + AvaloniaProperty.RegisterDirect( nameof(IsDropDownOpen), o => o.IsDropDownOpen, (o, v) => o.IsDropDownOpen = v); public static readonly StyledProperty IsTodayHighlightedProperty = - AvaloniaProperty.Register(nameof(IsTodayHighlighted)); - public static readonly DirectProperty SelectedDateProperty = - AvaloniaProperty.RegisterDirect( + AvaloniaProperty.Register(nameof(IsTodayHighlighted)); + public static readonly DirectProperty SelectedDateProperty = + AvaloniaProperty.RegisterDirect( nameof(SelectedDate), o => o.SelectedDate, (o, v) => o.SelectedDate = v); - public static readonly StyledProperty SelectedDateFormatProperty = - AvaloniaProperty.Register( + public static readonly StyledProperty SelectedDateFormatProperty = + AvaloniaProperty.Register( nameof(SelectedDateFormat), - defaultValue: DatePickerFormat.Short, + defaultValue: CalendarDatePickerFormat.Short, validate: IsValidSelectedDateFormat); public static readonly StyledProperty CustomDateFormatStringProperty = - AvaloniaProperty.Register( + AvaloniaProperty.Register( nameof(CustomDateFormatString), defaultValue: "d", validate: IsValidDateFormatString); - public static readonly DirectProperty TextProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty TextProperty = + AvaloniaProperty.RegisterDirect( nameof(Text), o => o.Text, (o, v) => o.Text = v); public static readonly StyledProperty WatermarkProperty = - TextBox.WatermarkProperty.AddOwner(); + TextBox.WatermarkProperty.AddOwner(); public static readonly StyledProperty UseFloatingWatermarkProperty = - TextBox.UseFloatingWatermarkProperty.AddOwner(); + TextBox.UseFloatingWatermarkProperty.AddOwner(); /// @@ -218,9 +218,9 @@ namespace Avalonia.Controls /// /// /// The specified date is not in the range defined by - /// + /// /// and - /// . + /// . /// public DateTime DisplayDate { @@ -320,7 +320,7 @@ namespace Avalonia.Controls /// /// An specified format is not valid. /// - public DatePickerFormat SelectedDateFormat + public CalendarDatePickerFormat SelectedDateFormat { get { return GetValue(SelectedDateFormatProperty); } set { SetValue(SelectedDateFormatProperty, value); } @@ -380,33 +380,33 @@ namespace Avalonia.Controls /// Occurs when /// is assigned a value that cannot be interpreted as a date. /// - public event EventHandler DateValidationError; + public event EventHandler DateValidationError; /// /// Occurs when the - /// + /// /// property is changed. /// public event EventHandler SelectedDateChanged; - static DatePicker() + static CalendarDatePicker() { - FocusableProperty.OverrideDefaultValue(true); - - DisplayDateProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateChanged(e)); - DisplayDateStartProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateStartChanged(e)); - DisplayDateEndProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateEndChanged(e)); - IsDropDownOpenProperty.Changed.AddClassHandler((x,e) => x.OnIsDropDownOpenChanged(e)); - SelectedDateProperty.Changed.AddClassHandler((x,e) => x.OnSelectedDateChanged(e)); - SelectedDateFormatProperty.Changed.AddClassHandler((x,e) => x.OnSelectedDateFormatChanged(e)); - CustomDateFormatStringProperty.Changed.AddClassHandler((x,e) => x.OnCustomDateFormatStringChanged(e)); - TextProperty.Changed.AddClassHandler((x,e) => x.OnTextChanged(e)); + FocusableProperty.OverrideDefaultValue(true); + + DisplayDateProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateChanged(e)); + DisplayDateStartProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateStartChanged(e)); + DisplayDateEndProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateEndChanged(e)); + IsDropDownOpenProperty.Changed.AddClassHandler((x,e) => x.OnIsDropDownOpenChanged(e)); + SelectedDateProperty.Changed.AddClassHandler((x,e) => x.OnSelectedDateChanged(e)); + SelectedDateFormatProperty.Changed.AddClassHandler((x,e) => x.OnSelectedDateFormatChanged(e)); + CustomDateFormatStringProperty.Changed.AddClassHandler((x,e) => x.OnCustomDateFormatStringChanged(e)); + TextProperty.Changed.AddClassHandler((x,e) => x.OnTextChanged(e)); } /// /// Initializes a new instance of the /// class. /// - public DatePicker() + public CalendarDatePicker() { FirstDayOfWeek = DateTimeHelper.GetCurrentDateFormat().FirstDayOfWeek; _defaultText = string.Empty; @@ -662,12 +662,12 @@ namespace Avalonia.Controls // change is coming from the Calendar UI itself, so, we // shouldn't change the DisplayDate since it will automatically // be changed by the Calendar - if ((day.Month != DisplayDate.Month || day.Year != DisplayDate.Year) && (_calendar == null || !_calendar.DatePickerDisplayDateFlag)) + if ((day.Month != DisplayDate.Month || day.Year != DisplayDate.Year) && (_calendar == null || !_calendar.CalendarDatePickerDisplayDateFlag)) { DisplayDate = day; } if(_calendar != null) - _calendar.DatePickerDisplayDateFlag = false; + _calendar.CalendarDatePickerDisplayDateFlag = false; } else { @@ -707,7 +707,7 @@ namespace Avalonia.Controls } private void OnCustomDateFormatStringChanged(AvaloniaPropertyChangedEventArgs e) { - if(SelectedDateFormat == DatePickerFormat.Custom) + if(SelectedDateFormat == CalendarDatePickerFormat.Custom) { OnDateFormatChanged(); } @@ -752,15 +752,15 @@ namespace Avalonia.Controls /// /// Raises the - /// + /// /// event. /// /// /// A - /// + /// /// that contains the event data. /// - protected virtual void OnDateValidationError(DatePickerDateValidationErrorEventArgs e) + protected virtual void OnDateValidationError(CalendarDatePickerDateValidationErrorEventArgs e) { DateValidationError?.Invoke(this, e); } @@ -959,7 +959,7 @@ namespace Avalonia.Controls } else { - var dateValidationError = new DatePickerDateValidationErrorEventArgs(new ArgumentOutOfRangeException(nameof(text), "SelectedDate value is not valid."), text); + var dateValidationError = new CalendarDatePickerDateValidationErrorEventArgs(new ArgumentOutOfRangeException(nameof(text), "SelectedDate value is not valid."), text); OnDateValidationError(dateValidationError); if (dateValidationError.ThrowException) @@ -970,7 +970,7 @@ namespace Avalonia.Controls } catch (FormatException ex) { - DatePickerDateValidationErrorEventArgs textParseError = new DatePickerDateValidationErrorEventArgs(ex, text); + CalendarDatePickerDateValidationErrorEventArgs textParseError = new CalendarDatePickerDateValidationErrorEventArgs(ex, text); OnDateValidationError(textParseError); if (textParseError.ThrowException) @@ -986,11 +986,11 @@ namespace Avalonia.Controls switch (SelectedDateFormat) { - case DatePickerFormat.Short: + case CalendarDatePickerFormat.Short: return string.Format(CultureInfo.CurrentCulture, d.ToString(dtfi.ShortDatePattern, dtfi)); - case DatePickerFormat.Long: + case CalendarDatePickerFormat.Long: return string.Format(CultureInfo.CurrentCulture, d.ToString(dtfi.LongDatePattern, dtfi)); - case DatePickerFormat.Custom: + case CalendarDatePickerFormat.Custom: return string.Format(CultureInfo.CurrentCulture, d.ToString(CustomDateFormatString, dtfi)); } return null; @@ -1118,12 +1118,12 @@ namespace Avalonia.Controls switch (SelectedDateFormat) { - case DatePickerFormat.Long: + case CalendarDatePickerFormat.Long: { watermarkText = string.Format(CultureInfo.CurrentCulture, watermarkFormat, dtfi.LongDatePattern.ToString()); break; } - case DatePickerFormat.Short: + case CalendarDatePickerFormat.Short: default: { watermarkText = string.Format(CultureInfo.CurrentCulture, watermarkFormat, dtfi.ShortDatePattern.ToString()); @@ -1139,11 +1139,11 @@ namespace Avalonia.Controls } } - private static bool IsValidSelectedDateFormat(DatePickerFormat value) + private static bool IsValidSelectedDateFormat(CalendarDatePickerFormat value) { - return value == DatePickerFormat.Long - || value == DatePickerFormat.Short - || value == DatePickerFormat.Custom; + return value == CalendarDatePickerFormat.Long + || value == CalendarDatePickerFormat.Short + || value == CalendarDatePickerFormat.Custom; } private static bool IsValidDateFormatString(string formatString) { diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index ece0ef97d9..0be7c4f67e 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -909,7 +909,7 @@ namespace Avalonia.Controls.Primitives case CalendarSelectionMode.SingleDate: { DateTime selectedDate = (DateTime)b.DataContext; - Owner.DatePickerDisplayDateFlag = true; + Owner.CalendarDatePickerDisplayDateFlag = true; if (Owner.SelectedDates.Count == 0) { Owner.SelectedDates.Add(selectedDate); @@ -981,7 +981,7 @@ namespace Avalonia.Controls.Primitives } case CalendarSelectionMode.SingleDate: { - Owner.DatePickerDisplayDateFlag = true; + Owner.CalendarDatePickerDisplayDateFlag = true; if (Owner.SelectedDates.Count == 0) { Owner.SelectedDates.Add(selectedDate); diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 1781067abb..e10d78917e 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -1228,7 +1228,7 @@ namespace Avalonia.Controls Debug.Assert(1 < count && 0 <= start && (start + count) <= definitions.Count); // avoid processing when asked to distribute "0" - if (!_IsZero(requestedSize)) + if (!MathUtilities.IsZero(requestedSize)) { DefinitionBase[] tempDefinitions = TempDefinitions; // temp array used to remember definitions for sorting int end = start + count; @@ -1306,7 +1306,7 @@ namespace Avalonia.Controls } // sanity check: requested size must all be distributed - Debug.Assert(_IsZero(sizeToDistribute)); + Debug.Assert(MathUtilities.IsZero(sizeToDistribute)); } else if (requestedSize <= rangeMaxSize) { @@ -1346,7 +1346,7 @@ namespace Avalonia.Controls } // sanity check: requested size must all be distributed - Debug.Assert(_IsZero(sizeToDistribute)); + Debug.Assert(MathUtilities.IsZero(sizeToDistribute)); } else { @@ -1358,7 +1358,7 @@ namespace Avalonia.Controls double equalSize = requestedSize / count; if (equalSize < maxMaxSize - && !_AreClose(equalSize, maxMaxSize)) + && !MathUtilities.AreClose(equalSize, maxMaxSize)) { // equi-size is less than maximum of maxSizes. // in this case distribute so that smaller definitions grow faster than @@ -2151,7 +2151,7 @@ namespace Avalonia.Controls // and precision of floating-point computation. (However, the resulting // display is subject to anti-aliasing problems. TANSTAAFL.) - if (!_AreClose(roundedTakenSize, finalSize)) + if (!MathUtilities.AreClose(roundedTakenSize, finalSize)) { // Compute deltas for (int i = 0; i < definitions.Count; ++i) @@ -2168,7 +2168,7 @@ namespace Avalonia.Controls if (roundedTakenSize > finalSize) { int i = definitions.Count - 1; - while ((adjustedSize > finalSize && !_AreClose(adjustedSize, finalSize)) && i >= 0) + while ((adjustedSize > finalSize && !MathUtilities.AreClose(adjustedSize, finalSize)) && i >= 0) { DefinitionBase definition = definitions[definitionIndices[i]]; double final = definition.SizeCache - dpiIncrement; @@ -2184,7 +2184,7 @@ namespace Avalonia.Controls else if (roundedTakenSize < finalSize) { int i = 0; - while ((adjustedSize < finalSize && !_AreClose(adjustedSize, finalSize)) && i < definitions.Count) + while ((adjustedSize < finalSize && !MathUtilities.AreClose(adjustedSize, finalSize)) && i < definitions.Count) { DefinitionBase definition = definitions[definitionIndices[i]]; double final = definition.SizeCache + dpiIncrement; @@ -2595,27 +2595,6 @@ namespace Avalonia.Controls set { SetFlags(value, Flags.HasGroup3CellsInAutoRows); } } - /// - /// fp version of d == 0. - /// - /// Value to check. - /// true if d == 0. - private static bool _IsZero(double d) - { - return (Math.Abs(d) < double.Epsilon); - } - - /// - /// fp version of d1 == d2 - /// - /// First value to compare - /// Second value to compare - /// true if d1 == d2 - private static bool _AreClose(double d1, double d2) - { - return (Math.Abs(d1 - d2) < double.Epsilon); - } - /// /// Returns reference to extended data bag. /// diff --git a/src/Avalonia.Controls/IndexPath.cs b/src/Avalonia.Controls/IndexPath.cs index 6c5aaf7ad1..73b75bc23d 100644 --- a/src/Avalonia.Controls/IndexPath.cs +++ b/src/Avalonia.Controls/IndexPath.cs @@ -123,6 +123,26 @@ namespace Avalonia.Controls } } + public bool IsAncestorOf(in IndexPath other) + { + if (other.GetSize() <= GetSize()) + { + return false; + } + + var size = GetSize(); + + for (int i = 0; i < size; i++) + { + if (GetAt(i) != other.GetAt(i)) + { + return false; + } + } + + return true; + } + public override string ToString() { if (_path != null) diff --git a/src/Avalonia.Controls/IndexRange.cs b/src/Avalonia.Controls/IndexRange.cs index 1dc161c699..e45d013af4 100644 --- a/src/Avalonia.Controls/IndexRange.cs +++ b/src/Avalonia.Controls/IndexRange.cs @@ -132,6 +132,53 @@ namespace Avalonia.Controls return result; } + public static int Intersect( + IList ranges, + IndexRange range, + IList? removed = null) + { + var result = 0; + + for (var i = 0; i < ranges.Count && range != s_invalid; ++i) + { + var existing = ranges[i]; + + if (existing.End < range.Begin || existing.Begin > range.End) + { + removed?.Add(existing); + ranges.RemoveAt(i--); + result += existing.Count; + } + else + { + if (existing.Begin < range.Begin) + { + var except = new IndexRange(existing.Begin, range.Begin - 1); + removed?.Add(except); + ranges[i] = existing = new IndexRange(range.Begin, existing.End); + result += except.Count; + } + + if (existing.End > range.End) + { + var except = new IndexRange(range.End + 1, existing.End); + removed?.Add(except); + ranges[i] = new IndexRange(existing.Begin, range.End); + result += except.Count; + } + } + } + + MergeRanges(ranges); + + if (removed is object) + { + MergeRanges(removed); + } + + return result; + } + public static int Remove( IList ranges, IndexRange range, diff --git a/src/Avalonia.Controls/SelectionModel.cs b/src/Avalonia.Controls/SelectionModel.cs index 93699583e6..ff1c0260bb 100644 --- a/src/Avalonia.Controls/SelectionModel.cs +++ b/src/Avalonia.Controls/SelectionModel.cs @@ -46,17 +46,25 @@ namespace Avalonia.Controls if (_rootNode.Source != null) { - if (_rootNode.Source != null) + // Temporarily prevent auto-select when switching source. + var restoreAutoSelect = _autoSelect; + _autoSelect = false; + + try { using (var operation = new Operation(this)) { ClearSelection(resetAnchor: true); } } + finally + { + _autoSelect = restoreAutoSelect; + } } _rootNode.Source = value; - ApplyAutoSelect(); + ApplyAutoSelect(true); RaisePropertyChanged("Source"); @@ -114,7 +122,7 @@ namespace Avalonia.Controls if (_autoSelect != value) { _autoSelect = value; - ApplyAutoSelect(); + ApplyAutoSelect(true); } } } @@ -133,7 +141,7 @@ namespace Avalonia.Controls while (current?.AnchorIndex >= 0) { path.Add(current.AnchorIndex); - current = current.GetAt(current.AnchorIndex, false); + current = current.GetAt(current.AnchorIndex, false, default); } anchor = new IndexPath(path); @@ -188,7 +196,6 @@ namespace Avalonia.Controls using var operation = new Operation(this); ClearSelection(resetAnchor: true); SelectWithPathImpl(value, select: true); - ApplyAutoSelect(); } } } @@ -384,21 +391,18 @@ namespace Avalonia.Controls { using var operation = new Operation(this); SelectImpl(index, select: false); - ApplyAutoSelect(); } public void Deselect(int groupIndex, int itemIndex) { using var operation = new Operation(this); SelectWithGroupImpl(groupIndex, itemIndex, select: false); - ApplyAutoSelect(); } public void DeselectAt(IndexPath index) { using var operation = new Operation(this); SelectWithPathImpl(index, select: false); - ApplyAutoSelect(); } public bool IsSelected(int index) => _rootNode.IsSelected(index); @@ -416,7 +420,7 @@ namespace Avalonia.Controls for (int i = 0; i < path.GetSize() - 1; i++) { var childIndex = path.GetAt(i); - node = node.GetAt(childIndex, realizeChild: false); + node = node.GetAt(childIndex, false, default); if (node == null) { @@ -451,7 +455,7 @@ namespace Avalonia.Controls } var isSelected = (bool?)false; - var childNode = _rootNode.GetAt(groupIndex, realizeChild: false); + var childNode = _rootNode.GetAt(groupIndex, false, default); if (childNode != null) { @@ -470,7 +474,7 @@ namespace Avalonia.Controls for (int i = 0; i < path.GetSize() - 1; i++) { var childIndex = path.GetAt(i); - node = node.GetAt(childIndex, realizeChild: false); + node = node.GetAt(childIndex, false, default); if (node == null) { @@ -565,7 +569,6 @@ namespace Avalonia.Controls { using var operation = new Operation(this); ClearSelection(resetAnchor: true); - ApplyAutoSelect(); } public IDisposable Update() => new Operation(this); @@ -592,10 +595,13 @@ namespace Avalonia.Controls } OnSelectionChanged(e); - ApplyAutoSelect(); + ApplyAutoSelect(true); } - internal IObservable? ResolvePath(object data, IndexPath dataIndexPath) + internal IObservable? ResolvePath( + object data, + IndexPath dataIndexPath, + IndexPath finalIndexPath) { IObservable? resolved = null; @@ -604,18 +610,22 @@ namespace Avalonia.Controls { if (_childrenRequestedEventArgs == null) { - _childrenRequestedEventArgs = new SelectionModelChildrenRequestedEventArgs(data, dataIndexPath, false); + _childrenRequestedEventArgs = new SelectionModelChildrenRequestedEventArgs( + data, + dataIndexPath, + finalIndexPath, + false); } else { - _childrenRequestedEventArgs.Initialize(data, dataIndexPath, false); + _childrenRequestedEventArgs.Initialize(data, dataIndexPath, finalIndexPath, false); } ChildrenRequested(this, _childrenRequestedEventArgs); resolved = _childrenRequestedEventArgs.Children; // Clear out the values in the args so that it cannot be used after the event handler call. - _childrenRequestedEventArgs.Initialize(null, default, true); + _childrenRequestedEventArgs.Initialize(null, default, default, true); } return resolved; @@ -632,6 +642,8 @@ namespace Avalonia.Controls { AnchorIndex = default; } + + OnSelectionChanged(); } private void OnSelectionChanged(SelectionModelSelectionChangedEventArgs? e = null) @@ -667,6 +679,8 @@ namespace Avalonia.Controls { AnchorIndex = new IndexPath(index); } + + OnSelectionChanged(); } private void SelectWithGroupImpl(int groupIndex, int itemIndex, bool select) @@ -676,13 +690,15 @@ namespace Avalonia.Controls ClearSelection(resetAnchor: true); } - var childNode = _rootNode.GetAt(groupIndex, realizeChild: true); + var childNode = _rootNode.GetAt(groupIndex, true, new IndexPath(groupIndex, itemIndex)); var selected = childNode!.Select(itemIndex, select); if (selected) { AnchorIndex = new IndexPath(groupIndex, itemIndex); } + + OnSelectionChanged(); } private void SelectWithPathImpl(IndexPath index, bool select) @@ -711,6 +727,8 @@ namespace Avalonia.Controls { AnchorIndex = index; } + + OnSelectionChanged(); } private void SelectRangeFromAnchorImpl(int index, bool select) @@ -724,6 +742,7 @@ namespace Avalonia.Controls } _rootNode.SelectRange(new IndexRange(anchorIndex, index), select); + OnSelectionChanged(); } private void SelectRangeFromAnchorWithGroupImpl(int endGroupIndex, int endItemIndex, bool select) @@ -752,11 +771,13 @@ namespace Avalonia.Controls for (int groupIdx = startGroupIndex; groupIdx <= endGroupIndex; groupIdx++) { - var groupNode = _rootNode.GetAt(groupIdx, realizeChild: true)!; + var groupNode = _rootNode.GetAt(groupIdx, true, new IndexPath(endGroupIndex, endItemIndex))!; int startIndex = groupIdx == startGroupIndex ? startItemIndex : 0; int endIndex = groupIdx == endGroupIndex ? endItemIndex : groupNode.DataCount - 1; groupNode.SelectRange(new IndexRange(startIndex, endIndex), select); } + + OnSelectionChanged(); } private void SelectRangeImpl(IndexPath start, IndexPath end, bool select) @@ -784,6 +805,8 @@ namespace Avalonia.Controls info.ParentNode!.Select(info.Path.GetAt(info.Path.GetSize() - 1), select); } }); + + OnSelectionChanged(); } private void BeginOperation() @@ -806,6 +829,8 @@ namespace Avalonia.Controls if (--_operationCount == 0) { + ApplyAutoSelect(false); + var changes = new List(); _rootNode.EndOperation(changes); @@ -827,7 +852,7 @@ namespace Avalonia.Controls } } - private void ApplyAutoSelect() + private void ApplyAutoSelect(bool createOperation) { if (AutoSelect) { @@ -835,8 +860,15 @@ namespace Avalonia.Controls if (SelectedIndex == default && _rootNode.ItemsSourceView?.Count > 0) { - using var operation = new Operation(this); - SelectImpl(0, true); + if (createOperation) + { + using var operation = new Operation(this); + SelectImpl(0, true); + } + else + { + SelectImpl(0, true); + } } } } diff --git a/src/Avalonia.Controls/SelectionModelChangeSet.cs b/src/Avalonia.Controls/SelectionModelChangeSet.cs index 6e77dc5755..d1df38656a 100644 --- a/src/Avalonia.Controls/SelectionModelChangeSet.cs +++ b/src/Avalonia.Controls/SelectionModelChangeSet.cs @@ -135,7 +135,7 @@ namespace Avalonia.Controls if (index >= currentIndex && index < currentIndex + currentCount) { int targetIndex = GetIndexAt(getRanges(info), index - currentIndex); - item = info.Items?.GetAt(targetIndex); + item = info.Items?.Count > targetIndex ? info.Items?.GetAt(targetIndex) : null; break; } diff --git a/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs b/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs index 974da0cf71..b1f3e0b2c4 100644 --- a/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs +++ b/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs @@ -16,15 +16,17 @@ namespace Avalonia.Controls { private object? _source; private IndexPath _sourceIndexPath; + private IndexPath _finalIndexPath; private bool _throwOnAccess; internal SelectionModelChildrenRequestedEventArgs( object source, IndexPath sourceIndexPath, + IndexPath finalIndexPath, bool throwOnAccess) { source = source ?? throw new ArgumentNullException(nameof(source)); - Initialize(source, sourceIndexPath, throwOnAccess); + Initialize(source, sourceIndexPath, finalIndexPath, throwOnAccess); } /// @@ -65,9 +67,26 @@ namespace Avalonia.Controls } } + /// + /// Gets the index of the final object which is being attempted to be retrieved. + /// + public IndexPath FinalIndex + { + get + { + if (_throwOnAccess) + { + throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs)); + } + + return _finalIndexPath; + } + } + internal void Initialize( object? source, IndexPath sourceIndexPath, + IndexPath finalIndexPath, bool throwOnAccess) { if (!throwOnAccess && source == null) @@ -77,6 +96,7 @@ namespace Avalonia.Controls _source = source; _sourceIndexPath = sourceIndexPath; + _finalIndexPath = finalIndexPath; _throwOnAccess = throwOnAccess; } } diff --git a/src/Avalonia.Controls/SelectionNode.cs b/src/Avalonia.Controls/SelectionNode.cs index 0b00db88c3..d99606673e 100644 --- a/src/Avalonia.Controls/SelectionNode.cs +++ b/src/Avalonia.Controls/SelectionNode.cs @@ -101,6 +101,7 @@ namespace Avalonia.Controls ItemsSourceView = newDataSource; + TrimInvalidSelections(); PopulateSelectedItemsFromSelectedIndices(); HookupCollectionChangedHandler(); OnSelectionChanged(); @@ -108,6 +109,26 @@ namespace Avalonia.Controls } } + private void TrimInvalidSelections() + { + if (_selected == null || ItemsSourceView == null) + { + return; + } + + var validRange = ItemsSourceView.Count > 0 ? new IndexRange(0, ItemsSourceView.Count - 1) : new IndexRange(-1, -1); + var removed = new List(); + var removedCount = IndexRange.Intersect(_selected, validRange, removed); + + if (removedCount > 0) + { + using var operation = _manager.Update(); + SelectedCount -= removedCount; + OnSelectionChanged(); + _operation!.Deselected(removed); + } + } + public ItemsSourceView? ItemsSourceView { get; private set; } public int DataCount => ItemsSourceView?.Count ?? 0; public int ChildrenNodeCount => _childrenNodes.Count; @@ -141,7 +162,7 @@ namespace Avalonia.Controls // create a bunch of leaf node instances - instead i use the same instance m_leafNode to avoid // an explosion of node objects. However, I'm still creating the m_childrenNodes // collection unfortunately. - public SelectionNode? GetAt(int index, bool realizeChild) + public SelectionNode? GetAt(int index, bool realizeChild, IndexPath finalIndexPath) { SelectionNode? child = null; @@ -171,7 +192,7 @@ namespace Avalonia.Controls if (childData != null) { var childDataIndexPath = IndexPath.CloneWithChildIndex(index); - resolver = _manager.ResolvePath(childData, childDataIndexPath); + resolver = _manager.ResolvePath(childData, childDataIndexPath, finalIndexPath); } if (resolver != null) @@ -843,7 +864,7 @@ namespace Avalonia.Controls int notSelectedCount = 0; for (int i = 0; i < ChildrenNodeCount; i++) { - var child = GetAt(i, realizeChild: false); + var child = GetAt(i, false, default); if (child != null) { diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index ec23bfa396..64378a4eb2 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -193,7 +193,8 @@ namespace Avalonia.Controls var orient = Orientation == Orientation.Horizontal; var pointDen = orient ? _track.Bounds.Width : _track.Bounds.Height; - pointDen += double.Epsilon; // Just add epsilon to avoid divide by zero exceptions. + // Just add epsilon to avoid NaN in case 0/0 + pointDen += double.Epsilon; var pointNum = orient ? x.Position.X : x.Position.Y; var logicalPos = MathUtilities.Clamp(pointNum / pointDen, 0.0d, 1.0d); diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 95e7437838..a91655855c 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -119,9 +119,10 @@ namespace Avalonia.Controls /// /// Gets or sets the selected item. /// - /// - /// Gets or sets the selected item. - /// + /// + /// Note that setting this property only currently works if the item is expanded to be visible. + /// To select non-expanded nodes use `Selection.SelectedIndex`. + /// public object SelectedItem { get => Selection.SelectedItem; @@ -346,7 +347,7 @@ namespace Avalonia.Controls if (container != null) { - container.BringIntoView(); + DispatcherTimer.RunOnce(container.BringIntoView, TimeSpan.Zero); } } } @@ -395,10 +396,17 @@ namespace Avalonia.Controls private void OnSelectionModelChildrenRequested(object sender, SelectionModelChildrenRequestedEventArgs e) { - var container = ItemContainerGenerator.Index.ContainerFromItem(e.Source) as ItemsControl; + var container = ItemContainerGenerator.Index.ContainerFromItem(e.Source) as TreeViewItem; if (container is object) { + if (e.SourceIndex.IsAncestorOf(e.FinalIndex)) + { + container.IsExpanded = true; + container.ApplyTemplate(); + container.Presenter?.ApplyTemplate(); + } + e.Children = Observable.CombineLatest( container.GetObservable(TreeViewItem.IsExpandedProperty), container.GetObservable(ItemsProperty), diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs index 51eb6edbea..438cbc8b27 100644 --- a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs +++ b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Controls.Utils { @@ -119,7 +120,7 @@ namespace Avalonia.Controls.Utils } var rect = new Rect(_size); - if (Math.Abs(borderThickness) > double.Epsilon) + if (!MathUtilities.IsZero(borderThickness)) rect = rect.Deflate(borderThickness * 0.5); var rrect = new RoundedRect(rect, _cornerRadius.TopLeft, _cornerRadius.TopRight, _cornerRadius.BottomRight, _cornerRadius.BottomLeft); diff --git a/src/Avalonia.Controls/Utils/SelectionTreeHelper.cs b/src/Avalonia.Controls/Utils/SelectionTreeHelper.cs index 430ecabbb8..5adf5bdeea 100644 --- a/src/Avalonia.Controls/Utils/SelectionTreeHelper.cs +++ b/src/Avalonia.Controls/Utils/SelectionTreeHelper.cs @@ -28,7 +28,7 @@ namespace Avalonia.Controls.Utils if (depth < path.GetSize() - 1) { - node = node.GetAt(childIndex, realizeChildren)!; + node = node.GetAt(childIndex, realizeChildren, path)!; } } } @@ -50,7 +50,7 @@ namespace Avalonia.Controls.Utils int count = realizeChildren ? nextNode.Node.DataCount : nextNode.Node.ChildrenNodeCount; for (int i = count - 1; i >= 0; i--) { - var child = nextNode.Node.GetAt(i, realizeChildren); + var child = nextNode.Node.GetAt(i, realizeChildren, nextNode.Path); var childPath = nextNode.Path.CloneWithChildIndex(i); if (child != null) { @@ -90,7 +90,7 @@ namespace Avalonia.Controls.Utils for (int i = endIndex; i >= startIndex; i--) { - var child = node.GetAt(i, realizeChild: true); + var child = node.GetAt(i, true, end); if (child != null) { var childPath = currentPath.CloneWithChildIndex(i); @@ -112,7 +112,7 @@ namespace Avalonia.Controls.Utils int endIndex = depth < end.GetSize() && isEndPath ? end.GetAt(depth) : info.Node.DataCount - 1; for (int i = endIndex; i >= startIndex; i--) { - var child = info.Node.GetAt(i, realizeChild: true); + var child = info.Node.GetAt(i, true, end); if (child != null) { var childPath = info.Path.CloneWithChildIndex(i); diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 7addaba977..1d19e1a346 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -2,6 +2,7 @@ using Avalonia.Controls; using Avalonia.Diagnostics.Models; using Avalonia.Input; +using Avalonia.Threading; namespace Avalonia.Diagnostics.ViewModels { @@ -49,7 +50,21 @@ namespace Avalonia.Diagnostics.ViewModels value is TreePageViewModel newTree && oldTree?.SelectedNode?.Visual is IControl control) { - newTree.SelectControl(control); + // HACK: We want to select the currently selected control in the new tree, but + // to select nested nodes in TreeView, currently the TreeView has to be able to + // expand the parent nodes. Because at this point the TreeView isn't visible, + // this will fail unless we schedule the selection to run after layout. + DispatcherTimer.RunOnce( + () => + { + try + { + newTree.SelectControl(control); + } + catch { } + }, + TimeSpan.FromMilliseconds(0), + DispatcherPriority.ApplicationIdle); } RaiseAndSetIfChanged(ref _content, value); diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index c8a9da600d..aa27538abc 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -1,11 +1,11 @@ using System; +using System.Collections.Generic; using System.Collections.Specialized; using System.Reactive; using System.Reactive.Linq; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.LogicalTree; -using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels @@ -82,5 +82,41 @@ namespace Avalonia.Diagnostics.ViewModels get; private set; } + + public IndexPath Index + { + get + { + var indices = new List(); + var child = this; + var parent = Parent; + + while (parent is object) + { + indices.Add(IndexOf(parent.Children, child)); + child = child.Parent; + parent = parent.Parent; + } + + indices.Add(0); + indices.Reverse(); + return new IndexPath(indices); + } + } + + private static int IndexOf(IReadOnlyList collection, TreeNode item) + { + var count = collection.Count; + + for (var i = 0; i < count; ++i) + { + if (collection[i] == item) + { + return i; + } + } + + throw new AvaloniaInternalException("TreeNode was not present in parent Children collection."); + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs index 26b5fe2524..38ac88a83c 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs @@ -6,28 +6,40 @@ namespace Avalonia.Diagnostics.ViewModels { internal class TreePageViewModel : ViewModelBase, IDisposable { - private TreeNode _selected; + private TreeNode _selectedNode; private ControlDetailsViewModel _details; private string _propertyFilter; public TreePageViewModel(TreeNode[] nodes) { Nodes = nodes; - } + Selection = new SelectionModel + { + SingleSelect = true, + Source = Nodes + }; + + Selection.SelectionChanged += (s, e) => + { + SelectedNode = (TreeNode)Selection.SelectedItem; + }; + } public TreeNode[] Nodes { get; protected set; } + public SelectionModel Selection { get; } + public TreeNode SelectedNode { - get => _selected; - set + get => _selectedNode; + private set { if (Details != null) { _propertyFilter = Details.PropertyFilter; } - if (RaiseAndSetIfChanged(ref _selected, value)) + if (RaiseAndSetIfChanged(ref _selectedNode, value)) { Details = value != null ? new ControlDetailsViewModel(value.Visual, _propertyFilter) : @@ -83,8 +95,8 @@ namespace Avalonia.Diagnostics.ViewModels if (node != null) { - SelectedNode = node; ExpandNode(node.Parent); + Selection.SelectedIndex = node.Index; } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml index a1e6ca7d37..4ddb320175 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml @@ -6,7 +6,7 @@ + Selection="{Binding Selection}"> diff --git a/src/Avalonia.Themes.Fluent/DatePicker.xaml b/src/Avalonia.Themes.Default/CalendarDatePicker.xaml similarity index 97% rename from src/Avalonia.Themes.Fluent/DatePicker.xaml rename to src/Avalonia.Themes.Default/CalendarDatePicker.xaml index 7adb1c2d5f..bc1aba1a03 100644 --- a/src/Avalonia.Themes.Fluent/DatePicker.xaml +++ b/src/Avalonia.Themes.Default/CalendarDatePicker.xaml @@ -8,7 +8,7 @@ - - diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 67279fca99..83da5d3142 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -45,7 +45,7 @@ - + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml index 1d42fd3af1..d07b9dcc86 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml @@ -92,6 +92,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml index ae4300bcf4..af18261f7e 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml @@ -93,6 +93,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index 0c0b7a1044..5ca531c598 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -39,6 +39,87 @@ + + 1 + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 21 64 diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index 6fdb7a001a..5e1d268953 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -39,6 +39,86 @@ + + 1 + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 21 64 diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml index 5b6da70233..d29db79e7b 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml @@ -5,4 +5,5 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml index 5825a2bf34..43867f6e97 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml @@ -5,4 +5,5 @@ + diff --git a/src/Avalonia.Themes.Fluent/Button.xaml b/src/Avalonia.Themes.Fluent/Button.xaml index 42fcee6cd5..ee8060f823 100644 --- a/src/Avalonia.Themes.Fluent/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Button.xaml @@ -24,21 +24,19 @@ + --> - - + @@ -46,19 +44,19 @@ @@ -70,19 +68,19 @@ diff --git a/src/Avalonia.Themes.Default/DatePicker.xaml b/src/Avalonia.Themes.Fluent/CalendarDatePicker.xaml similarity index 97% rename from src/Avalonia.Themes.Default/DatePicker.xaml rename to src/Avalonia.Themes.Fluent/CalendarDatePicker.xaml index 7adb1c2d5f..bc1aba1a03 100644 --- a/src/Avalonia.Themes.Default/DatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/CalendarDatePicker.xaml @@ -8,7 +8,7 @@ - - diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml index 266acce971..36ae114f9b 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml @@ -45,7 +45,7 @@ - + diff --git a/src/Avalonia.Themes.Fluent/RepeatButton.xaml b/src/Avalonia.Themes.Fluent/RepeatButton.xaml index 702e4e6ebd..70ab8090f1 100644 --- a/src/Avalonia.Themes.Fluent/RepeatButton.xaml +++ b/src/Avalonia.Themes.Fluent/RepeatButton.xaml @@ -1,40 +1,62 @@ - - - - + + + + + + + + + + 8,5,8,6 + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Slider.xaml b/src/Avalonia.Themes.Fluent/Slider.xaml index a57ea6cedd..539c448e0f 100644 --- a/src/Avalonia.Themes.Fluent/Slider.xaml +++ b/src/Avalonia.Themes.Fluent/Slider.xaml @@ -64,7 +64,7 @@ - + @@ -76,7 +76,7 @@ - + @@ -125,7 +125,7 @@ - + @@ -137,7 +137,7 @@ - + diff --git a/src/Avalonia.Themes.Fluent/ToggleButton.xaml b/src/Avalonia.Themes.Fluent/ToggleButton.xaml index 9e05c38eef..2e9bd4c825 100644 --- a/src/Avalonia.Themes.Fluent/ToggleButton.xaml +++ b/src/Avalonia.Themes.Fluent/ToggleButton.xaml @@ -1,38 +1,109 @@ - + + + + + + + + + + + + + 8,5,8,6 + - + + + + + + + + + + + + + + + + + + + - - \ No newline at end of file + diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index 6fdcd9631b..b1cf1aecc9 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -4,6 +4,7 @@ using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Rendering.SceneGraph; using Avalonia.Threading; +using Avalonia.Utilities; using Avalonia.Visuals.Media.Imaging; namespace Avalonia.Media @@ -154,12 +155,12 @@ namespace Avalonia.Media return; } - if (Math.Abs(radiusX) > double.Epsilon) + if (!MathUtilities.IsZero(radiusX)) { radiusX = Math.Min(radiusX, rect.Width / 2); } - if (Math.Abs(radiusY) > double.Epsilon) + if (!MathUtilities.IsZero(radiusY)) { radiusY = Math.Min(radiusY, rect.Height / 2); } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index 720185a3ad..0292398782 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -4,6 +4,7 @@ using System.Linq; using Avalonia.Media.Immutable; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; +using Avalonia.Utilities; using Avalonia.Utility; namespace Avalonia.Media.TextFormatting @@ -184,7 +185,7 @@ namespace Avalonia.Media.TextFormatting /// private void UpdateLayout() { - if (_text.IsEmpty || Math.Abs(MaxWidth) < double.Epsilon || Math.Abs(MaxHeight) < double.Epsilon) + if (_text.IsEmpty || MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) { var textLine = CreateEmptyTextLine(0); diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index 9ab5882361..7aa1eed890 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -107,6 +107,14 @@ namespace Avalonia.Data private object ConvertValue(IList values, Type targetType, IMultiValueConverter converter) { + for (var i = 0; i < values.Count; ++i) + { + if (values[i] is BindingNotification notification) + { + values[i] = notification.Value; + } + } + var culture = CultureInfo.CurrentCulture; var converted = converter.Convert(values, targetType, ConverterParameter, culture); diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index bbb45cf64c..9b7ba4844a 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -236,7 +236,7 @@ namespace Avalonia.Direct2D1.Media Math.Max(rrect.RadiiTopRight.X, Math.Max(rrect.RadiiBottomRight.X, rrect.RadiiBottomLeft.X))); var radiusY = Math.Max(rrect.RadiiTopLeft.Y, Math.Max(rrect.RadiiTopRight.Y, Math.Max(rrect.RadiiBottomRight.Y, rrect.RadiiBottomLeft.Y))); - var isRounded = Math.Abs(radiusX) > double.Epsilon || Math.Abs(radiusY) > double.Epsilon; + var isRounded = !MathUtilities.IsZero(radiusX) || !MathUtilities.IsZero(radiusY); if (brush != null) { diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs index 6e175707e4..df871a67b4 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs @@ -322,7 +322,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { "bar", new BindingNotification( - new MissingMemberException("Could not find CLR property 'Bar' on 'Avalonia.Base.UnitTests.Data.Core.ExpressionObserverTests_Property+WithoutBar'"), + new MissingMemberException("Could not find a matching property accessor for 'Bar' on 'Avalonia.Base.UnitTests.Data.Core.ExpressionObserverTests_Property+WithoutBar'"), BindingErrorType.Error), "baz", }, diff --git a/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs new file mode 100644 index 0000000000..0378a5b017 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs @@ -0,0 +1,119 @@ +using System; +using Avalonia.Utilities; +using Xunit; + +namespace Avalonia.Base.UnitTests.Utilities +{ + public class MathUtilitiesTests + { + private const double AnyValue = 42.42; + private readonly double _calculatedAnyValue; + private readonly double _one; + private readonly double _zero; + + public MathUtilitiesTests() + { + _calculatedAnyValue = 0.0; + _one = 0.0; + _zero = 1.0; + + const int N = 10; + var dxAny = AnyValue / N; + var dxOne = 1.0 / N; + var dxZero = _zero / N; + + for (var i = 0; i < N; ++i) + { + _calculatedAnyValue += dxAny; + _one += dxOne; + _zero -= dxZero; + } + } + + [Fact] + public void Two_Equivalent_Double_Values_Are_Close() + { + var actual = MathUtilities.AreClose(AnyValue, _calculatedAnyValue); + + Assert.True(actual); + Assert.Equal(AnyValue, Math.Round(_calculatedAnyValue, 14)); + } + + [Fact] + public void Two_Equivalent_Single_Values_Are_Close() + { + var expectedValue = (float)AnyValue; + var actualValue = (float)_calculatedAnyValue; + + var actual = MathUtilities.AreClose(expectedValue, actualValue); + + Assert.True(actual); + Assert.Equal((float) Math.Round(expectedValue, 5), (float) Math.Round(actualValue, 4)); + } + + [Fact] + public void Calculated_Double_One_Is_One() + { + var actual = MathUtilities.IsOne(_one); + + Assert.True(actual); + Assert.Equal(1.0, Math.Round(_one, 15)); + } + + [Fact] + public void Calculated_Single_One_Is_One() + { + var actualValue = (float)_one; + + var actual = MathUtilities.IsOne(actualValue); + + Assert.True(actual); + Assert.Equal(1.0f, (float) Math.Round(actualValue, 7)); + } + + [Fact] + public void Calculated_Double_Zero_Is_Zero() + { + var actual = MathUtilities.IsZero(_zero); + + Assert.True(actual); + Assert.Equal(0.0, Math.Round(_zero, 15)); + } + + [Fact] + public void Calculated_Single_Zero_Is_Zero() + { + var actualValue = (float)_zero; + + var actual = MathUtilities.IsZero(actualValue); + + Assert.True(actual); + Assert.Equal(0.0f, (float) Math.Round(actualValue, 7)); + } + + [Fact] + public void Clamp_Input_NaN_Return_NaN() + { + var clamp = MathUtilities.Clamp(double.NaN, 0.0, 1.0); + Assert.True(double.IsNaN(clamp)); + } + + [Fact] + public void Clamp_Input_NegativeInfinity_Return_Min() + { + const double min = 0.0; + const double max = 1.0; + var actual = MathUtilities.Clamp(double.NegativeInfinity, min, max); + Assert.Equal(min, actual); + } + + [Fact] + public void Clamp_Input_PositiveInfinity_Return_Max() + { + const double min = 0.0; + const double max = 1.0; + var actual = MathUtilities.Clamp(double.PositiveInfinity, min, max); + Assert.Equal(max, actual); + } + } +} diff --git a/tests/Avalonia.Benchmarks/Data/AccessorTestObject.cs b/tests/Avalonia.Benchmarks/Data/AccessorTestObject.cs new file mode 100644 index 0000000000..0039f5670c --- /dev/null +++ b/tests/Avalonia.Benchmarks/Data/AccessorTestObject.cs @@ -0,0 +1,47 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; + +namespace Avalonia.Benchmarks.Data +{ + internal class AccessorTestObject : INotifyPropertyChanged + { + private string _test; + + public string Test + { + get => _test; + set + { + if (_test == value) + { + return; + } + + _test = value; + + OnPropertyChanged(); + } + } + + public event PropertyChangedEventHandler PropertyChanged; + + public void Execute() + { + } + + public void Execute(object p0) + { + } + + public void Execute(object p0, object p1) + { + } + + [NotifyPropertyChangedInvocator] + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/tests/Avalonia.Benchmarks/Data/PropertyAccessorBenchmarks.cs b/tests/Avalonia.Benchmarks/Data/PropertyAccessorBenchmarks.cs index 3d5a4029bb..6c5e0d3b53 100644 --- a/tests/Avalonia.Benchmarks/Data/PropertyAccessorBenchmarks.cs +++ b/tests/Avalonia.Benchmarks/Data/PropertyAccessorBenchmarks.cs @@ -1,9 +1,6 @@ using System; -using System.ComponentModel; -using System.Runtime.CompilerServices; using Avalonia.Data.Core.Plugins; using BenchmarkDotNet.Attributes; -using JetBrains.Annotations; namespace Avalonia.Benchmarks.Data { @@ -12,7 +9,7 @@ namespace Avalonia.Benchmarks.Data { private readonly InpcPropertyAccessorPlugin _inpcPlugin = new InpcPropertyAccessorPlugin(); private readonly MethodAccessorPlugin _methodPlugin = new MethodAccessorPlugin(); - private readonly TestObject _targetStrongRef = new TestObject(); + private readonly AccessorTestObject _targetStrongRef = new AccessorTestObject(); private readonly WeakReference _targetWeakRef; public PropertyAccessorBenchmarks() @@ -23,66 +20,25 @@ namespace Avalonia.Benchmarks.Data [Benchmark] public void InpcAccessorMatch() { - _inpcPlugin.Match(_targetWeakRef, nameof(TestObject.Test)); + _inpcPlugin.Match(_targetWeakRef, nameof(AccessorTestObject.Test)); } [Benchmark] public void InpcAccessorStart() { - _inpcPlugin.Start(_targetWeakRef, nameof(TestObject.Test)); + _inpcPlugin.Start(_targetWeakRef, nameof(AccessorTestObject.Test)); } [Benchmark] public void MethodAccessorMatch() { - _methodPlugin.Match(_targetWeakRef, nameof(TestObject.Execute)); + _methodPlugin.Match(_targetWeakRef, nameof(AccessorTestObject.Execute)); } [Benchmark] public void MethodAccessorStart() { - _methodPlugin.Start(_targetWeakRef, nameof(TestObject.Execute)); - } - - private class TestObject : INotifyPropertyChanged - { - private string _test; - - public string Test - { - get => _test; - set - { - if (_test == value) - { - return; - } - - _test = value; - - OnPropertyChanged(); - } - } - - public void Execute() - { - } - - public void Execute(object p0) - { - } - - public void Execute(object p0, object p1) - { - } - - public event PropertyChangedEventHandler PropertyChanged; - - [NotifyPropertyChangedInvocator] - protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } + _methodPlugin.Start(_targetWeakRef, nameof(AccessorTestObject.Execute)); } } } diff --git a/tests/Avalonia.Benchmarks/Data/PropertyAccessorPluginBenchmarks.cs b/tests/Avalonia.Benchmarks/Data/PropertyAccessorPluginBenchmarks.cs new file mode 100644 index 0000000000..75e3269e3d --- /dev/null +++ b/tests/Avalonia.Benchmarks/Data/PropertyAccessorPluginBenchmarks.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using Avalonia.Data.Core.Plugins; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Data +{ + [MemoryDiagnoser, InProcess] + public class PropertyAccessorPluginBenchmarks + { + private readonly AccessorTestObject _targetStrongRef = new AccessorTestObject(); + + private readonly List _oldPlugins; + private readonly List _newPlugins; + + public PropertyAccessorPluginBenchmarks() + { + _oldPlugins = new List + { + new AvaloniaPropertyAccessorPlugin(), + new MethodAccessorPlugin(), + new InpcPropertyAccessorPlugin() + }; + + _newPlugins = new List + { + new AvaloniaPropertyAccessorPlugin(), + new InpcPropertyAccessorPlugin(), + new MethodAccessorPlugin() + }; + } + + [Benchmark] + public void MatchAccessorOld() + { + var propertyName = nameof(AccessorTestObject.Test); + + foreach (IPropertyAccessorPlugin x in _oldPlugins) + { + if (x.Match(_targetStrongRef, propertyName)) + { + break; + } + } + } + + [Benchmark] + public void MatchAccessorNew() + { + var propertyName = nameof(AccessorTestObject.Test); + + foreach (IPropertyAccessorPlugin x in _newPlugins) + { + if (x.Match(_targetStrongRef, propertyName)) + { + break; + } + } + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs b/tests/Avalonia.Controls.UnitTests/CalendarDatePickerTests.cs similarity index 90% rename from tests/Avalonia.Controls.UnitTests/DatePickerTests.cs rename to tests/Avalonia.Controls.UnitTests/CalendarDatePickerTests.cs index 3d396a9726..f41a3e7581 100644 --- a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CalendarDatePickerTests.cs @@ -15,7 +15,7 @@ using Xunit; namespace Avalonia.Controls.UnitTests { - public class DatePickerTests + public class CalendarDatePickerTests { private static bool CompareDates(DateTime first, DateTime second) { @@ -30,7 +30,7 @@ namespace Avalonia.Controls.UnitTests using (UnitTestApplication.Start(Services)) { bool handled = false; - DatePicker datePicker = CreateControl(); + CalendarDatePicker datePicker = CreateControl(); datePicker.SelectedDateChanged += (s,e) => { handled = true; @@ -47,7 +47,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(Services)) { - DatePicker datePicker = CreateControl(); + CalendarDatePicker datePicker = CreateControl(); datePicker.BlackoutDates.AddDatesInPast(); DateTime goodValue = DateTime.Today.AddDays(1); @@ -65,7 +65,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(Services)) { - DatePicker datePicker = CreateControl(); + CalendarDatePicker datePicker = CreateControl(); datePicker.SelectedDate = DateTime.Today.AddDays(5); Assert.ThrowsAny( @@ -76,10 +76,10 @@ namespace Avalonia.Controls.UnitTests private static TestServices Services => TestServices.MockThreadingInterface.With( standardCursorFactory: Mock.Of()); - private DatePicker CreateControl() + private CalendarDatePicker CreateControl() { var datePicker = - new DatePicker + new CalendarDatePicker { Template = CreateTemplate() }; @@ -90,7 +90,7 @@ namespace Avalonia.Controls.UnitTests private IControlTemplate CreateTemplate() { - return new FuncControlTemplate((control, scope) => + return new FuncControlTemplate((control, scope) => { var textBox = new TextBox diff --git a/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs b/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs index e0f46d9fa9..e01c752658 100644 --- a/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs @@ -127,6 +127,88 @@ namespace Avalonia.Controls.UnitTests Assert.Empty(selected); } + [Fact] + public void Intersect_Should_Remove_Items_From_Beginning() + { + var ranges = new List { new IndexRange(0, 10) }; + var removed = new List(); + var result = IndexRange.Intersect(ranges, new IndexRange(2, 12), removed); + + Assert.Equal(2, result); + Assert.Equal(new[] { new IndexRange(2, 10) }, ranges); + Assert.Equal(new[] { new IndexRange(0, 1) }, removed); + } + + [Fact] + public void Intersect_Should_Remove_Items_From_End() + { + var ranges = new List { new IndexRange(0, 10) }; + var removed = new List(); + var result = IndexRange.Intersect(ranges, new IndexRange(0, 8), removed); + + Assert.Equal(2, result); + Assert.Equal(new[] { new IndexRange(0, 8) }, ranges); + Assert.Equal(new[] { new IndexRange(9, 10) }, removed); + } + + [Fact] + public void Intersect_Should_Remove_Entire_Range_Start() + { + var ranges = new List { new IndexRange(0, 5), new IndexRange(6, 10) }; + var removed = new List(); + var result = IndexRange.Intersect(ranges, new IndexRange(6, 10), removed); + + Assert.Equal(6, result); + Assert.Equal(new[] { new IndexRange(6, 10) }, ranges); + Assert.Equal(new[] { new IndexRange(0, 5) }, removed); + } + + [Fact] + public void Intersect_Should_Remove_Entire_Range_End() + { + var ranges = new List { new IndexRange(0, 5), new IndexRange(6, 10) }; + var removed = new List(); + var result = IndexRange.Intersect(ranges, new IndexRange(0, 4), removed); + + Assert.Equal(6, result); + Assert.Equal(new[] { new IndexRange(0, 4) }, ranges); + Assert.Equal(new[] { new IndexRange(5, 10) }, removed); + } + + [Fact] + public void Intersect_Should_Remove_Entire_Range_Start_End() + { + var ranges = new List + { + new IndexRange(0, 2), + new IndexRange(3, 7), + new IndexRange(8, 10) + }; + var removed = new List(); + var result = IndexRange.Intersect(ranges, new IndexRange(3, 7), removed); + + Assert.Equal(6, result); + Assert.Equal(new[] { new IndexRange(3, 7) }, ranges); + Assert.Equal(new[] { new IndexRange(0, 2), new IndexRange(8, 10) }, removed); + } + + [Fact] + public void Intersect_Should_Remove_Entire_And_Partial_Range_Start_End() + { + var ranges = new List + { + new IndexRange(0, 2), + new IndexRange(3, 7), + new IndexRange(8, 10) + }; + var removed = new List(); + var result = IndexRange.Intersect(ranges, new IndexRange(4, 6), removed); + + Assert.Equal(8, result); + Assert.Equal(new[] { new IndexRange(4, 6) }, ranges); + Assert.Equal(new[] { new IndexRange(0, 3), new IndexRange(7, 10) }, removed); + } + [Fact] public void Remove_Should_Remove_Entire_Range() { diff --git a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs index ebf9c40012..24e82a69d0 100644 --- a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs @@ -1512,6 +1512,47 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, raised); } + [Fact] + public void Batch_Update_Selection_Is_Correct_Throughout() + { + var data = new[] { "foo", "bar", "baz", "qux" }; + var target = new SelectionModel { Source = data }; + var raised = 0; + + using (target.Update()) + { + target.Select(1); + + Assert.Equal(new IndexPath(1), target.SelectedIndex); + Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices); + Assert.Equal("bar", target.SelectedItem); + Assert.Equal(new[] { "bar" }, target.SelectedItems); + + target.Deselect(1); + + Assert.Equal(new IndexPath(), target.SelectedIndex); + Assert.Empty(target.SelectedIndices); + Assert.Null(target.SelectedItem); + Assert.Empty(target.SelectedItems); + + target.SelectRange(new IndexPath(1), new IndexPath(1)); + + Assert.Equal(new IndexPath(1), target.SelectedIndex); + Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices); + Assert.Equal("bar", target.SelectedItem); + Assert.Equal(new[] { "bar" }, target.SelectedItems); + + target.ClearSelection(); + + Assert.Equal(new IndexPath(), target.SelectedIndex); + Assert.Empty(target.SelectedIndices); + Assert.Null(target.SelectedItem); + Assert.Empty(target.SelectedItems); + } + + Assert.Equal(0, raised); + } + [Fact] public void AutoSelect_Selects_When_Enabled() { @@ -1713,6 +1754,30 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, raised); } + [Fact] + public void AutoSelect_Is_Applied_At_End_Of_Batch_Update() + { + var data = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel { AutoSelect = true, Source = data }; + + using (target.Update()) + { + target.ClearSelection(); + + Assert.Equal(new IndexPath(), target.SelectedIndex); + Assert.Empty(target.SelectedIndices); + Assert.Null(target.SelectedItem); + Assert.Empty(target.SelectedItems); + } + + Assert.Equal(new IndexPath(0), target.SelectedIndex); + Assert.Equal(new[] { new IndexPath(0) }, target.SelectedIndices); + Assert.Equal("foo", target.SelectedItem); + Assert.Equal(new[] { "foo" }, target.SelectedItems); + + Assert.Equal(new IndexPath(0), target.SelectedIndex); + } + [Fact] public void Can_Replace_Parent_Children_Collection() { @@ -1806,6 +1871,87 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, node.PropertyChangedSubscriptions); } + [Fact] + public void Setting_SelectedIndex_To_Minus_1_Clears_Selection() + { + var data = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel { Source = data }; + target.SelectedIndex = new IndexPath(1); + target.SelectedIndex = new IndexPath(-1); + Assert.Empty(target.SelectedIndices); + } + + [Fact] + public void Assigning_Source_With_Less_Items_Than_Previous_Clears_Selection() + { + var data = new[] { "foo", "bar", "baz", "boo", "hoo" }; + var smallerData = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel { RetainSelectionOnReset = true }; + target.Source = data; + target.SelectedIndex = new IndexPath(4); + target.Source = smallerData; + Assert.Empty(target.SelectedIndices); + } + + [Fact] + public void Initializing_Source_With_Less_Items_Than_Selection_Trims_Selection() + { + var data = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel(); + target.SelectedIndex = new IndexPath(4); + target.Source = data; + Assert.Empty(target.SelectedIndices); + } + + [Fact] + public void Initializing_Source_With_Less_Items_Than_Selection_Trims_Selection_RetainSelection() + { + var data = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel { RetainSelectionOnReset = true }; + target.SelectedIndex = new IndexPath(4); + target.Source = data; + Assert.Empty(target.SelectedIndices); + } + + [Fact] + public void Initializing_Source_With_Less_Items_Than_Multiple_Selection_Trims_Selection() + { + var data = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel { RetainSelectionOnReset = true }; + target.Select(4); + target.Select(2); + target.Source = data; + Assert.Equal(1, target.SelectedIndices.Count); + Assert.Equal(new IndexPath(2), target.SelectedIndices.First()); + } + + [Fact] + public void Initializing_Source_With_Less_Items_Than_Selection_Raises_SelectionChanged() + { + var data = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel(); + var raised = 0; + + target.SelectedIndex = new IndexPath(4); + + target.SelectionChanged += (s, e) => + { + if (raised == 0) + { + Assert.Equal(new[] { Path(4) }, e.DeselectedIndices); + Assert.Equal(new object[] { null }, e.DeselectedItems); + Assert.Empty(e.SelectedIndices); + Assert.Empty(e.SelectedItems); + } + + ++raised; + }; + + target.Source = data; + + Assert.Equal(2, raised); + } + private int GetSubscriberCount(AvaloniaList list) { return ((INotifyCollectionChangedDebug)list).GetCollectionChangedSubscribers()?.Length ?? 0; diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index 24aacd4000..db9211ac3c 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -1,10 +1,12 @@ using System; +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.Controls.Utils; using Avalonia.LogicalTree; using Avalonia.Styling; using Avalonia.UnitTests; @@ -325,6 +327,28 @@ namespace Avalonia.Controls.UnitTests Assert.NotEqual(dataContext, tabItem.Content); } + [Fact] + public void Can_Have_Empty_Tab_Control() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new Markup.Xaml.AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var tabControl = window.FindControl("tabs"); + + tabControl.DataContext = new { Tabs = new List() }; + window.ApplyTemplate(); + + Assert.Equal(0, tabControl.Items.Count()); + } + } + private IControlTemplate TabControlTemplate() { return new FuncControlTemplate((parent, scope) => diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 373f3e6861..c1bd45bcad 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -118,313 +118,340 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Clicking_Item_Should_Select_It() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var item = tree[0].Children[1].Children[0]; - var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + var item = tree[0].Children[1].Children[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); - Assert.NotNull(container); + Assert.NotNull(container); - _mouse.Click(container); + _mouse.Click(container); - Assert.Equal(item, target.SelectedItem); - Assert.True(container.IsSelected); + Assert.Equal(item, target.SelectedItem); + Assert.True(container.IsSelected); + } } [Fact] public void Clicking_WithControlModifier_Selected_Item_Should_Deselect_It() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var item = tree[0].Children[1].Children[0]; - var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + var item = tree[0].Children[1].Children[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); - Assert.NotNull(container); + Assert.NotNull(container); - target.SelectedItem = item; + target.SelectedItem = item; - Assert.True(container.IsSelected); + Assert.True(container.IsSelected); - _mouse.Click(container, modifiers: KeyModifiers.Control); + _mouse.Click(container, modifiers: KeyModifiers.Control); - Assert.Null(target.SelectedItem); - Assert.False(container.IsSelected); + Assert.Null(target.SelectedItem); + Assert.False(container.IsSelected); + } } [Fact] public void Clicking_WithControlModifier_Not_Selected_Item_Should_Select_It() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var item1 = tree[0].Children[1].Children[0]; - var container1 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1); + var item1 = tree[0].Children[1].Children[0]; + var container1 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1); - var item2 = tree[0].Children[1]; - var container2 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2); + var item2 = tree[0].Children[1]; + var container2 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2); - Assert.NotNull(container1); - Assert.NotNull(container2); + Assert.NotNull(container1); + Assert.NotNull(container2); - target.SelectedItem = item1; + target.SelectedItem = item1; - Assert.True(container1.IsSelected); + Assert.True(container1.IsSelected); - _mouse.Click(container2, modifiers: KeyModifiers.Control); - - Assert.Equal(item2, target.SelectedItem); - Assert.False(container1.IsSelected); - Assert.True(container2.IsSelected); + _mouse.Click(container2, modifiers: KeyModifiers.Control); + + Assert.Equal(item2, target.SelectedItem); + Assert.False(container1.IsSelected); + Assert.True(container2.IsSelected); + } } [Fact] public void Clicking_WithControlModifier_Selected_Item_Should_Deselect_And_Remove_From_SelectedItems() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var rootNode = tree[0]; + var rootNode = tree[0]; - var item1 = rootNode.Children[0]; - var item2 = rootNode.Children.Last(); + var item1 = rootNode.Children[0]; + var item2 = rootNode.Children.Last(); - var item1Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1); - var item2Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2); + var item1Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1); + var item2Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2); - ClickContainer(item1Container, KeyModifiers.Control); - Assert.True(item1Container.IsSelected); + ClickContainer(item1Container, KeyModifiers.Control); + Assert.True(item1Container.IsSelected); - ClickContainer(item2Container, KeyModifiers.Control); - Assert.True(item2Container.IsSelected); + ClickContainer(item2Container, KeyModifiers.Control); + Assert.True(item2Container.IsSelected); - Assert.Equal(new[] {item1, item2}, target.Selection.SelectedItems.OfType()); + Assert.Equal(new[] { item1, item2 }, target.Selection.SelectedItems.OfType()); - ClickContainer(item1Container, KeyModifiers.Control); - Assert.False(item1Container.IsSelected); + ClickContainer(item1Container, KeyModifiers.Control); + Assert.False(item1Container.IsSelected); - Assert.DoesNotContain(item1, target.Selection.SelectedItems.OfType()); + Assert.DoesNotContain(item1, target.Selection.SelectedItems.OfType()); + } } [Fact] public void Clicking_WithShiftModifier_DownDirection_Should_Select_Range_Of_Items() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var rootNode = tree[0]; + var rootNode = tree[0]; - var from = rootNode.Children[0]; - var to = rootNode.Children.Last(); + var from = rootNode.Children[0]; + var to = rootNode.Children.Last(); - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(fromContainer, KeyModifiers.None); - Assert.True(fromContainer.IsSelected); + Assert.True(fromContainer.IsSelected); - ClickContainer(toContainer, KeyModifiers.Shift); - AssertChildrenSelected(target, rootNode); + ClickContainer(toContainer, KeyModifiers.Shift); + AssertChildrenSelected(target, rootNode); + } } [Fact] public void Clicking_WithShiftModifier_UpDirection_Should_Select_Range_Of_Items() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var rootNode = tree[0]; + var rootNode = tree[0]; - var from = rootNode.Children.Last(); - var to = rootNode.Children[0]; + var from = rootNode.Children.Last(); + var to = rootNode.Children[0]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(fromContainer, KeyModifiers.None); - Assert.True(fromContainer.IsSelected); + Assert.True(fromContainer.IsSelected); - ClickContainer(toContainer, KeyModifiers.Shift); - AssertChildrenSelected(target, rootNode); + ClickContainer(toContainer, KeyModifiers.Shift); + AssertChildrenSelected(target, rootNode); + } } [Fact] public void Clicking_First_Item_Of_SelectedItems_Should_Select_Only_It() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var rootNode = tree[0]; + var rootNode = tree[0]; - var from = rootNode.Children.Last(); - var to = rootNode.Children[0]; + var from = rootNode.Children.Last(); + var to = rootNode.Children[0]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(fromContainer, KeyModifiers.None); - ClickContainer(toContainer, KeyModifiers.Shift); - AssertChildrenSelected(target, rootNode); + ClickContainer(toContainer, KeyModifiers.Shift); + AssertChildrenSelected(target, rootNode); - ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(fromContainer, KeyModifiers.None); - Assert.True(fromContainer.IsSelected); + Assert.True(fromContainer.IsSelected); - foreach (var child in rootNode.Children) - { - if (child == from) + foreach (var child in rootNode.Children) { - continue; - } + if (child == from) + { + continue; + } - var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(child); + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(child); - Assert.False(container.IsSelected); + Assert.False(container.IsSelected); + } } } [Fact] public void Setting_SelectedItem_Should_Set_Container_Selected() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var item = tree[0].Children[1].Children[0]; - var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + var item = tree[0].Children[1].Children[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); - Assert.NotNull(container); + Assert.NotNull(container); - target.SelectedItem = item; + target.SelectedItem = item; - Assert.True(container.IsSelected); + Assert.True(container.IsSelected); + } } [Fact] public void Setting_SelectedItem_Should_Raise_SelectedItemChanged_Event() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var item = tree[0].Children[1].Children[0]; + var item = tree[0].Children[1].Children[0]; - var called = false; - target.SelectionChanged += (s, e) => - { - Assert.Empty(e.RemovedItems); - Assert.Equal(1, e.AddedItems.Count); - Assert.Same(item, e.AddedItems[0]); - called = true; - }; + var called = false; + target.SelectionChanged += (s, e) => + { + Assert.Empty(e.RemovedItems); + Assert.Equal(1, e.AddedItems.Count); + Assert.Same(item, e.AddedItems[0]); + called = true; + }; - target.SelectedItem = item; - Assert.True(called); + target.SelectedItem = item; + Assert.True(called); + } } [Fact] @@ -564,7 +591,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Keyboard_Navigation_Should_Move_To_Last_Selected_Node() { - using (UnitTestApplication.Start(TestServices.RealFocus)) + using (Application()) { var focus = FocusManager.Instance; var navigation = AvaloniaLocator.Current.GetService(); @@ -647,7 +674,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Pressing_SelectAll_Gesture_With_Downward_Range_Selected_Should_Select_All_Nodes() { - using (UnitTestApplication.Start()) + using (Application()) { var tree = CreateTestTreeData(); var target = new TreeView @@ -694,7 +721,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Pressing_SelectAll_Gesture_With_Upward_Range_Selected_Should_Select_All_Nodes() { - using (UnitTestApplication.Start()) + using (Application()) { var tree = CreateTestTreeData(); var target = new TreeView @@ -768,97 +795,106 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Right_Click_On_UnselectedItem_Should_Clear_Existing_Selection() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple, - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); - var rootNode = tree[0]; - var to = rootNode.Children[0]; - var then = rootNode.Children[1]; + var rootNode = tree[0]; + var to = rootNode.Children[0]; + var then = rootNode.Children[1]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(rootNode); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - var thenContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(then); + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(rootNode); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var thenContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(then); - ClickContainer(fromContainer, KeyModifiers.None); - ClickContainer(toContainer, KeyModifiers.Shift); + ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(toContainer, KeyModifiers.Shift); - Assert.Equal(2, target.Selection.SelectedItems.Count); + Assert.Equal(2, target.Selection.SelectedItems.Count); - _mouse.Click(thenContainer, MouseButton.Right); + _mouse.Click(thenContainer, MouseButton.Right); - Assert.Equal(1, target.Selection.SelectedItems.Count); + Assert.Equal(1, target.Selection.SelectedItems.Count); + } } [Fact] public void Shift_Right_Click_Should_Not_Select_Multiple() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple, - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); - var rootNode = tree[0]; - var from = rootNode.Children[0]; - var to = rootNode.Children[1]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var rootNode = tree[0]; + var from = rootNode.Children[0]; + var to = rootNode.Children[1]; + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - _mouse.Click(fromContainer); - _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Shift); + _mouse.Click(fromContainer); + _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Shift); - Assert.Equal(1, target.Selection.SelectedItems.Count); + Assert.Equal(1, target.Selection.SelectedItems.Count); + } } [Fact] public void Ctrl_Right_Click_Should_Not_Select_Multiple() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple, - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); - var rootNode = tree[0]; - var from = rootNode.Children[0]; - var to = rootNode.Children[1]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var rootNode = tree[0]; + var from = rootNode.Children[0]; + var to = rootNode.Children[1]; + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - _mouse.Click(fromContainer); - _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Control); + _mouse.Click(fromContainer); + _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Control); - Assert.Equal(1, target.Selection.SelectedItems.Count); + Assert.Equal(1, target.Selection.SelectedItems.Count); + } } [Fact] @@ -944,7 +980,7 @@ namespace Avalonia.Controls.UnitTests public void Auto_Expanding_In_Style_Should_Not_Break_Range_Selection() { /// Issue #2980. - using (UnitTestApplication.Start(TestServices.RealStyler)) + using (Application()) { var target = new DerivedTreeView { @@ -1183,12 +1219,12 @@ namespace Avalonia.Controls.UnitTests } } - void ClickContainer(IControl container, KeyModifiers modifiers) + private void ClickContainer(IControl container, KeyModifiers modifiers) { _mouse.Click(container, modifiers: modifiers); } - void AssertChildrenSelected(TreeView treeView, Node rootNode) + private void AssertChildrenSelected(TreeView treeView, Node rootNode) { foreach (var child in rootNode.Children) { @@ -1198,6 +1234,16 @@ namespace Avalonia.Controls.UnitTests } } + private IDisposable Application() + { + return UnitTestApplication.Start( + TestServices.MockThreadingInterface.With( + focusManager: new FocusManager(), + keyboardDevice: () => new KeyboardDevice(), + keyboardNavigation: new KeyboardNavigationHandler(), + inputManager: new InputManager())); + } + private class Node : NotifyingBase { private IAvaloniaList _children; diff --git a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs index 5773f5e6b0..64f5eb2a0e 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs @@ -113,6 +113,50 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal("(null)", target.Text); } + [Fact] + public void Should_Pass_UnsetValue_To_Converter_For_Broken_Binding() + { + var source = new { A = 1, B = 2, C = 3 }; + var target = new TextBlock { DataContext = source }; + + var binding = new MultiBinding + { + Converter = new ConcatConverter(), + Bindings = new[] + { + new Binding { Path = "A" }, + new Binding { Path = "B" }, + new Binding { Path = "Missing" }, + }, + }; + + target.Bind(TextBlock.TextProperty, binding); + + Assert.Equal("1,2,(unset)", target.Text); + } + + [Fact] + public void Should_Pass_FallbackValue_To_Converter_For_Broken_Binding() + { + var source = new { A = 1, B = 2, C = 3 }; + var target = new TextBlock { DataContext = source }; + + var binding = new MultiBinding + { + Converter = new ConcatConverter(), + Bindings = new[] + { + new Binding { Path = "A" }, + new Binding { Path = "B" }, + new Binding { Path = "Missing", FallbackValue = "Fallback" }, + }, + }; + + target.Bind(TextBlock.TextProperty, binding); + + Assert.Equal("1,2,Fallback", target.Text); + } + private class ConcatConverter : IMultiValueConverter { public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs index 4f7264f2f2..8cf74c36f4 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs @@ -22,7 +22,7 @@ namespace Avalonia.Markup.UnitTests.Parsers Assert.Equal( new BindingNotification( - new MissingMemberException("Could not find CLR property 'Baz' on '1'"), BindingErrorType.Error), + new MissingMemberException("Could not find a matching property accessor for 'Baz' on '1'"), BindingErrorType.Error), result); GC.KeepAlive(data); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs index 43c7525939..6730e3134d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs @@ -1,6 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; using Avalonia.Controls; +using Avalonia.Controls.Presenters; using Avalonia.Data; +using Avalonia.Data.Converters; using Avalonia.UnitTests; +using Avalonia.VisualTree; using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.Data @@ -54,5 +61,51 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data Assert.Equal("bar", textBlock.Text); } } + + [Fact] + public void MultiBinding_TemplatedParent_Works() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBox = window.FindControl("textBox"); + + window.ApplyTemplate(); + textBox.ApplyTemplate(); + + var target = (TextPresenter)textBox.GetVisualChildren().Single(); + Assert.Equal("Foo,Bar", target.Text); + } + } + } + + public class ConcatConverter : IMultiValueConverter + { + public static ConcatConverter Instance { get; } = new ConcatConverter(); + + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + return string.Join(",", values); + } } }