ソースを参照

fixing data validation for Combobox control and all SelectingItems control #5652

aljosas 4 年 前
コミット
899ba64913

+ 20 - 8
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);
 
         /// <summary>
         /// Defines the <see cref="SelectedItems"/> property.
@@ -466,6 +466,20 @@ namespace Avalonia.Controls.Primitives
             EndUpdating();
         }
 
+        /// <summary>
+        /// Called to update the validation state for properties for which data validation is
+        /// enabled.
+        /// </summary>
+        /// <param name="property">The property.</param>
+        /// <param name="value">The new binding value for the property.</param>
+        protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> 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<IEnumerable>();
@@ -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<ISelectionModel> Selection { get; set; }
             public Optional<IList?> SelectedItems { get; set; }
 
-            public Optional<int> SelectedIndex 
+            public Optional<int> SelectedIndex
             {
                 get => _selectedIndex;
                 set
@@ -996,6 +1008,6 @@ namespace Avalonia.Controls.Primitives
                     _selectedIndex = default;
                 }
             }
-       }
+        }
     }
 }

+ 44 - 66
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<Carousel>(CreateTemplate),
-                Items = new[]
-                {
-                    "Foo",
-                    "Bar"
-                }
+                Template = new FuncControlTemplate<Carousel>(CreateTemplate), Items = new[] { "Foo", "Bar" }
             };
 
             target.ApplyTemplate();
@@ -35,12 +34,7 @@ namespace Avalonia.Controls.UnitTests
         {
             var target = new Carousel
             {
-                Template = new FuncControlTemplate<Carousel>(CreateTemplate),
-                Items = new[]
-                {
-                    "Foo",
-                    "Bar"
-                }
+                Template = new FuncControlTemplate<Carousel>(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<string>
-            {
-               "Foo",
-               "Bar",
-               "FooBar"
-            };
+            var items = new ObservableCollection<string> { "Foo", "Bar", "FooBar" };
 
             var target = new Carousel
             {
-                Template = new FuncControlTemplate<Carousel>(CreateTemplate),
-                Items = items,
-                IsVirtualized = false
+                Template = new FuncControlTemplate<Carousel>(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<string>
-            {
-               "Foo",
-               "Bar",
-               "FooBar"
-            };
+            var items = new ObservableCollection<string> { "Foo", "Bar", "FooBar" };
 
             var target = new Carousel
             {
-                Template = new FuncControlTemplate<Carousel>(CreateTemplate),
-                Items = items,
-                IsVirtualized = true,
+                Template = new FuncControlTemplate<Carousel>(CreateTemplate), Items = items, IsVirtualized = true,
             };
 
             target.ApplyTemplate();
@@ -150,9 +130,7 @@ namespace Avalonia.Controls.UnitTests
             var items = new ObservableCollection<string>();
             var target = new Carousel
             {
-                Template = new FuncControlTemplate<Carousel>(CreateTemplate),
-                Items = items,
-                IsVirtualized = false
+                Template = new FuncControlTemplate<Carousel>(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<string>
-            {
-               "Foo",
-               "Bar",
-               "FooBar"
-            };
+            var items = new ObservableCollection<string> { "Foo", "Bar", "FooBar" };
 
             var target = new Carousel
             {
-                Template = new FuncControlTemplate<Carousel>(CreateTemplate),
-                Items = items,
-                IsVirtualized = false
+                Template = new FuncControlTemplate<Carousel>(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<string>
-            {
-               "Foo",
-               "Bar",
-               "FooBar"
-            };
+            var items = new ObservableCollection<string> { "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<string>
-            {
-               "Foo",
-               "Bar",
-               "FooBar"
-            };
+            var items = new ObservableCollection<string> { "Foo", "Bar", "FooBar" };
 
             var target = new Carousel
             {
-                Template = new FuncControlTemplate<Carousel>(CreateTemplate),
-                Items = items,
-                IsVirtualized = false
+                Template = new FuncControlTemplate<Carousel>(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<string>
-            {
-               "Foo",
-               "Bar",
-               "FooBar"
-            };
+            var items = new ObservableCollection<string> { "Foo", "Bar", "FooBar" };
 
             var target = new Carousel
             {
-                Template = new FuncControlTemplate<Carousel>(CreateTemplate),
-                Items = items,
-                IsVirtualized = false
+                Template = new FuncControlTemplate<Carousel>(CreateTemplate), Items = items, IsVirtualized = false
             };
 
             target.ApplyTemplate();
@@ -311,5 +263,31 @@ namespace Avalonia.Controls.UnitTests
             contentPresenter.UpdateChild();
             return Assert.IsType<TextBlock>(contentPresenter.Child);
         }
+
+        [Fact]
+        public void SelectedItem_Validation()
+        {
+            using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
+            {
+                var target = new Carousel
+                {
+                    Template = new FuncControlTemplate<Carousel>(CreateTemplate), IsVirtualized = false
+                };
+
+                target.ApplyTemplate();
+                target.Presenter.ApplyTemplate();
+
+                var exception = new System.InvalidCastException("failed validation");
+                var textObservable =
+                    new BehaviorSubject<BindingNotification>(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 }));
+            }
+        }
     }
 }

+ 65 - 0
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<BindingNotification>(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<ButtonSpinner>()
+                // .Select(b => b.Content)
+                .OfType<TextBox>()
+                .First();
+        }
+        // private IControlTemplate CreateTemplate()
+        // {
+        //     return new FuncControlTemplate<ComboBox>((control, scope) =>
+        //     {
+        //         var textBox =
+        //             new TextBox
+        //             {
+        //                 Name = "PART_TextBox"
+        //             }.RegisterInNameScope(scope);
+        //         return new ButtonSpinner
+        //         {
+        //             Name = "PART_Spinner",
+        //             Content = textBox,
+        //         }.RegisterInNameScope(scope);
+        //     });
+        // }
     }
 }

+ 7 - 0
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@@ -559,5 +559,12 @@ namespace Avalonia.Controls.UnitTests
 
             public string Value { get; }
         }
+
+
+        [Fact]
+        public void SelectedItem_Validation()
+        {
+            
+        }
     }
 }