From 899ba649130a4327cd2d755e3c6ea76b10ddda60 Mon Sep 17 00:00:00 2001 From: aljosas Date: Fri, 12 Mar 2021 12:15:12 +0100 Subject: [PATCH] fixing data validation for Combobox control and all SelectingItems control #5652 --- .../Primitives/SelectingItemsControl.cs | 28 +++-- .../CarouselTests.cs | 110 +++++++----------- .../ComboBoxTests.cs | 65 +++++++++++ .../ListBoxTests.cs | 7 ++ 4 files changed, 136 insertions(+), 74 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 2cd69793dc..ccdff8c9b7 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -64,7 +64,7 @@ namespace Avalonia.Controls.Primitives nameof(SelectedItem), o => o.SelectedItem, (o, v) => o.SelectedItem = v, - defaultBindingMode: BindingMode.TwoWay); + defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); /// /// Defines the property. @@ -466,6 +466,20 @@ namespace Avalonia.Controls.Primitives EndUpdating(); } + /// + /// 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 == SelectedItemProperty) + { + DataValidationErrors.SetError(this, value.Error); + } + } + protected override void OnInitialized() { base.OnInitialized(); @@ -503,6 +517,7 @@ namespace Avalonia.Controls.Primitives { AutoScrollToSelectedItemIfNecessary(); } + if (change.Property == ItemsProperty && _updateState is null && _selection is object) { var newValue = change.NewValue.GetValueOrDefault(); @@ -707,7 +722,7 @@ namespace Avalonia.Controls.Primitives _oldSelectedItem = SelectedItem; } else if (e.PropertyName == nameof(InternalSelectionModel.WritableSelectedItems) && - _oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems) + _oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems) { RaisePropertyChanged( SelectedItemsProperty, @@ -853,10 +868,7 @@ namespace Avalonia.Controls.Primitives private ISelectionModel CreateDefaultSelectionModel() { - return new InternalSelectionModel - { - SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple), - }; + return new InternalSelectionModel { SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple), }; } private void InitializeSelectionModel(ISelectionModel model) @@ -977,7 +989,7 @@ namespace Avalonia.Controls.Primitives public Optional Selection { get; set; } public Optional SelectedItems { get; set; } - public Optional SelectedIndex + public Optional SelectedIndex { get => _selectedIndex; set @@ -996,6 +1008,6 @@ namespace Avalonia.Controls.Primitives _selectedIndex = default; } } - } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs index 051f6c3fd3..393fde0faf 100644 --- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs @@ -1,10 +1,14 @@ using System.Collections.ObjectModel; using System.Linq; +using System.Reactive.Subjects; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.LogicalTree; +using Avalonia.Threading; using Avalonia.VisualTree; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Controls.UnitTests @@ -16,12 +20,7 @@ namespace Avalonia.Controls.UnitTests { var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), - Items = new[] - { - "Foo", - "Bar" - } + Template = new FuncControlTemplate(CreateTemplate), Items = new[] { "Foo", "Bar" } }; target.ApplyTemplate(); @@ -35,12 +34,7 @@ namespace Avalonia.Controls.UnitTests { var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), - Items = new[] - { - "Foo", - "Bar" - } + Template = new FuncControlTemplate(CreateTemplate), Items = new[] { "Foo", "Bar" } }; target.ApplyTemplate(); @@ -75,18 +69,11 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Item_Changes_To_First_Item_When_Items_Property_Changes() { - var items = new ObservableCollection - { - "Foo", - "Bar", - "FooBar" - }; + var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false }; target.ApplyTemplate(); @@ -111,18 +98,11 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Item_Changes_To_First_Item_When_Items_Property_Changes_And_Virtualized() { - var items = new ObservableCollection - { - "Foo", - "Bar", - "FooBar" - }; + var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = true, + Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = true, }; target.ApplyTemplate(); @@ -150,9 +130,7 @@ namespace Avalonia.Controls.UnitTests var items = new ObservableCollection(); var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false }; target.ApplyTemplate(); @@ -170,18 +148,11 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Index_Changes_To_None_When_Items_Assigned_Null() { - var items = new ObservableCollection - { - "Foo", - "Bar", - "FooBar" - }; + var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false }; target.ApplyTemplate(); @@ -204,12 +175,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Index_Is_Maintained_Carousel_Created_With_Non_Zero_SelectedIndex() { - var items = new ObservableCollection - { - "Foo", - "Bar", - "FooBar" - }; + var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; var target = new Carousel { @@ -233,18 +199,11 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Item_Changes_To_Next_First_Item_When_Item_Removed_From_Beggining_Of_List() { - var items = new ObservableCollection - { - "Foo", - "Bar", - "FooBar" - }; + var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false }; target.ApplyTemplate(); @@ -267,18 +226,11 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Item_Changes_To_First_Item_If_SelectedItem_Is_Removed_From_Middle() { - var items = new ObservableCollection - { - "Foo", - "Bar", - "FooBar" - }; + var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false }; target.ApplyTemplate(); @@ -311,5 +263,31 @@ namespace Avalonia.Controls.UnitTests contentPresenter.UpdateChild(); return Assert.IsType(contentPresenter.Child); } + + [Fact] + public void SelectedItem_Validation() + { + using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) + { + var target = new Carousel + { + Template = new FuncControlTemplate(CreateTemplate), IsVirtualized = false + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + var exception = new System.InvalidCastException("failed validation"); + var textObservable = + new BehaviorSubject(new BindingNotification(exception, + BindingErrorType.DataValidationError)); + target.Bind(ComboBox.SelectedItemProperty, textObservable); + + Dispatcher.UIThread.RunJobs(); + + Assert.True(DataValidationErrors.GetHasErrors(target)); + Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index 4ea838358c..9f90037032 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -1,11 +1,14 @@ using System.Linq; +using System.Reactive.Subjects; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Media; +using Avalonia.Threading; using Avalonia.UnitTests; using Xunit; @@ -173,5 +176,67 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(expectedSelectedIndex, target.SelectedIndex); } } + + [Fact] + public void SelectedItem_Validation() + { + + using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) + { + var target = new ComboBox + { + Template = GetTemplate() + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + var exception = new System.InvalidCastException("failed validation"); + var textObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError)); + target.Bind(ComboBox.SelectedItemProperty, textObservable); + + Dispatcher.UIThread.RunJobs(); + + Assert.True(DataValidationErrors.GetHasErrors(target)); + Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); + + } + + } + private ComboBox CreateControl() + { + var control = new ComboBox() + { + Template = GetTemplate() + }; + + control.ApplyTemplate(); + return control; + } + + private TextBox GetTextBox(ComboBox control) + { + return control.GetTemplateChildren() + // .OfType() + // .Select(b => b.Content) + .OfType() + .First(); + } + // private IControlTemplate CreateTemplate() + // { + // return new FuncControlTemplate((control, scope) => + // { + // var textBox = + // new TextBox + // { + // Name = "PART_TextBox" + // }.RegisterInNameScope(scope); + // return new ButtonSpinner + // { + // Name = "PART_Spinner", + // Content = textBox, + // }.RegisterInNameScope(scope); + // }); + // } } } diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 145fce4fed..d13f5b704d 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -559,5 +559,12 @@ namespace Avalonia.Controls.UnitTests public string Value { get; } } + + + [Fact] + public void SelectedItem_Validation() + { + + } } }