From 5d0c8121e62c26745332695439a54651c1fd09f1 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Tue, 7 May 2024 20:53:37 +0100 Subject: [PATCH] Housekeeping Retire Xamarin (#652) * Housekeeping Retire Xamarin * Incorrectly place null check --- .editorconfig | 354 +++++++++++++++- .../Extensions/ViewForExtensions.cs | 181 -------- ...eactiveUI.Validation.AndroidSupport.csproj | 19 - .../Extensions/ViewForExtensions.cs | 181 -------- .../ReactiveUI.Validation.AndroidX.csproj | 18 - ...s.ValidationProject.DotNet6_0.verified.txt | 26 +- ...s.ValidationProject.DotNet7_0.verified.txt | 26 +- ...s.ValidationProject.DotNet8_0.verified.txt | 26 +- .../MemoryLeakTests.cs | 64 +++ .../Models/SourceDestinationViewModel.cs | 4 +- .../Models/TestClassMemory.cs | 60 +++ .../Models/TestViewModel.cs | 6 +- .../NotifyDataErrorInfoTests.cs | 10 +- .../ReactiveUI.Validation.Tests.csproj | 4 +- .../ValidationBindingTests.cs | 28 +- .../ValidationTextTests.cs | 47 +-- src/ReactiveUI.Validation.sln | 16 +- .../Abstractions/IValidatableViewModel.cs | 4 +- .../Collections/ReadOnlyCollectionPooled.cs | 14 +- .../Collections/ValidationText.cs | 16 +- .../IPropertyValidationComponent.cs | 4 +- .../Components/BasePropertyValidation.cs | 8 +- .../Components/ObservableValidation.cs | 385 ------------------ ...rvableValidationBase{TViewModel,TValue}.cs | 171 ++++++++ ...ableValidation{TViewModel,TValue,TProp}.cs | 123 ++++++ ...ObservableValidation{TViewModel,TValue}.cs | 118 ++++++ .../Contexts/IValidationContext.cs | 59 +++ .../Contexts/ValidationContext.cs | 56 +-- .../Extensions/ArrayPoolExtensions.cs | 4 +- .../ValidatableViewModelExtensions.cs | 6 +- .../Extensions/ValidationContextExtensions.cs | 7 +- .../Helpers/ReactiveValidationObject.cs | 45 +- .../Helpers/ValidationHelper.cs | 5 +- .../ReactiveUI.Validation.csproj | 11 +- .../States/IValidationState.cs | 1 + .../ValidationBindings/ValidationBinding.cs | 9 +- version.json | 2 +- 37 files changed, 1159 insertions(+), 959 deletions(-) delete mode 100644 src/ReactiveUI.Validation.AndroidSupport/Extensions/ViewForExtensions.cs delete mode 100644 src/ReactiveUI.Validation.AndroidSupport/ReactiveUI.Validation.AndroidSupport.csproj delete mode 100644 src/ReactiveUI.Validation.AndroidX/Extensions/ViewForExtensions.cs delete mode 100644 src/ReactiveUI.Validation.AndroidX/ReactiveUI.Validation.AndroidX.csproj create mode 100644 src/ReactiveUI.Validation.Tests/MemoryLeakTests.cs create mode 100644 src/ReactiveUI.Validation.Tests/Models/TestClassMemory.cs delete mode 100644 src/ReactiveUI.Validation/Components/ObservableValidation.cs create mode 100644 src/ReactiveUI.Validation/Components/ObservableValidationBase{TViewModel,TValue}.cs create mode 100644 src/ReactiveUI.Validation/Components/ObservableValidation{TViewModel,TValue,TProp}.cs create mode 100644 src/ReactiveUI.Validation/Components/ObservableValidation{TViewModel,TValue}.cs create mode 100644 src/ReactiveUI.Validation/Contexts/IValidationContext.cs diff --git a/.editorconfig b/.editorconfig index 3a2d4f8e..f1d3d943 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,6 +10,39 @@ root = true insert_final_newline = true indent_style = space indent_size = 4 +dotnet_diagnostic.CA1027.severity=error +dotnet_diagnostic.CA1062.severity=error +dotnet_diagnostic.CA1064.severity=error +dotnet_diagnostic.CA1066.severity=error +dotnet_diagnostic.CA1067.severity=error +dotnet_diagnostic.CA1068.severity=error +dotnet_diagnostic.CA1069.severity=warning +dotnet_diagnostic.CA2013.severity=error +dotnet_diagnostic.CA1802.severity=error +dotnet_diagnostic.CA1813.severity=error +dotnet_diagnostic.CA1814.severity=error +dotnet_diagnostic.CA1815.severity=error +dotnet_diagnostic.CA1822.severity=error +dotnet_diagnostic.CA1827.severity=error +dotnet_diagnostic.CA1828.severity=error +dotnet_diagnostic.CA1826.severity=error +dotnet_diagnostic.CA1829.severity=error +dotnet_diagnostic.CA1830.severity=error +dotnet_diagnostic.CA1831.severity=error +dotnet_diagnostic.CA1832.severity=error +dotnet_diagnostic.CA1833.severity=error +dotnet_diagnostic.CA1834.severity=error +dotnet_diagnostic.CA1835.severity=error +dotnet_diagnostic.CA1836.severity=error +dotnet_diagnostic.CA1837.severity=error +dotnet_diagnostic.CA1838.severity=error +dotnet_diagnostic.CA2015.severity=error +dotnet_diagnostic.CA2012.severity=error +dotnet_diagnostic.CA2011.severity=error +dotnet_diagnostic.CA2009.severity=error +dotnet_diagnostic.CA2008.severity=error +dotnet_diagnostic.CA2007.severity=warning +dotnet_diagnostic.CA2000.severity=suggestion [project.json] indent_size = 2 @@ -43,14 +76,11 @@ dotnet_style_qualification_for_method = false:suggestion dotnet_style_qualification_for_event = false:suggestion # only use var when it's obvious what the variable type is -csharp_style_var_for_built_in_types = false:none +csharp_style_var_for_built_in_types = true:suggestion csharp_style_var_when_type_is_apparent = true:suggestion csharp_style_var_elsewhere = true:suggestion -# Types: use keywords instead of BCL types, and permit var only when the type is clear -csharp_style_var_for_built_in_types = false:suggestion -csharp_style_var_when_type_is_apparent = false:none -csharp_style_var_elsewhere = false:suggestion +# prefer C# premade types. dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion dotnet_style_predefined_type_for_member_access = true:suggestion @@ -157,6 +187,318 @@ csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_between_square_brackets = false +# analyzers +dotnet_diagnostic.AvoidAsyncVoid.severity = suggestion + +dotnet_diagnostic.CA1000.severity = none +dotnet_diagnostic.CA1001.severity = error +dotnet_diagnostic.CA1009.severity = error +dotnet_diagnostic.CA1016.severity = error +dotnet_diagnostic.CA1030.severity = none +dotnet_diagnostic.CA1031.severity = none +dotnet_diagnostic.CA1033.severity = none +dotnet_diagnostic.CA1036.severity = none +dotnet_diagnostic.CA1049.severity = error +dotnet_diagnostic.CA1056.severity = suggestion +dotnet_diagnostic.CA1060.severity = error +dotnet_diagnostic.CA1061.severity = error +dotnet_diagnostic.CA1063.severity = error +dotnet_diagnostic.CA1065.severity = error +dotnet_diagnostic.CA1301.severity = error +dotnet_diagnostic.CA1303.severity = none +dotnet_diagnostic.CA1308.severity = none +dotnet_diagnostic.CA1400.severity = error +dotnet_diagnostic.CA1401.severity = error +dotnet_diagnostic.CA1403.severity = error +dotnet_diagnostic.CA1404.severity = error +dotnet_diagnostic.CA1405.severity = error +dotnet_diagnostic.CA1410.severity = error +dotnet_diagnostic.CA1415.severity = error +dotnet_diagnostic.CA1507.severity = error +dotnet_diagnostic.CA1710.severity = suggestion +dotnet_diagnostic.CA1724.severity = none +dotnet_diagnostic.CA1810.severity = none +dotnet_diagnostic.CA1821.severity = error +dotnet_diagnostic.CA1900.severity = error +dotnet_diagnostic.CA1901.severity = error +dotnet_diagnostic.CA2000.severity = none +dotnet_diagnostic.CA2002.severity = error +dotnet_diagnostic.CA2007.severity = none +dotnet_diagnostic.CA2100.severity = error +dotnet_diagnostic.CA2101.severity = error +dotnet_diagnostic.CA2108.severity = error +dotnet_diagnostic.CA2111.severity = error +dotnet_diagnostic.CA2112.severity = error +dotnet_diagnostic.CA2114.severity = error +dotnet_diagnostic.CA2116.severity = error +dotnet_diagnostic.CA2117.severity = error +dotnet_diagnostic.CA2122.severity = error +dotnet_diagnostic.CA2123.severity = error +dotnet_diagnostic.CA2124.severity = error +dotnet_diagnostic.CA2126.severity = error +dotnet_diagnostic.CA2131.severity = error +dotnet_diagnostic.CA2132.severity = error +dotnet_diagnostic.CA2133.severity = error +dotnet_diagnostic.CA2134.severity = error +dotnet_diagnostic.CA2137.severity = error +dotnet_diagnostic.CA2138.severity = error +dotnet_diagnostic.CA2140.severity = error +dotnet_diagnostic.CA2141.severity = error +dotnet_diagnostic.CA2146.severity = error +dotnet_diagnostic.CA2147.severity = error +dotnet_diagnostic.CA2149.severity = error +dotnet_diagnostic.CA2200.severity = error +dotnet_diagnostic.CA2202.severity = error +dotnet_diagnostic.CA2207.severity = error +dotnet_diagnostic.CA2212.severity = error +dotnet_diagnostic.CA2213.severity = error +dotnet_diagnostic.CA2214.severity = error +dotnet_diagnostic.CA2216.severity = error +dotnet_diagnostic.CA2220.severity = error +dotnet_diagnostic.CA2229.severity = error +dotnet_diagnostic.CA2231.severity = error +dotnet_diagnostic.CA2232.severity = error +dotnet_diagnostic.CA2235.severity = error +dotnet_diagnostic.CA2236.severity = error +dotnet_diagnostic.CA2237.severity = error +dotnet_diagnostic.CA2238.severity = error +dotnet_diagnostic.CA2240.severity = error +dotnet_diagnostic.CA2241.severity = error +dotnet_diagnostic.CA2242.severity = error + +dotnet_diagnostic.RCS1001.severity = error +dotnet_diagnostic.RCS1018.severity = error +dotnet_diagnostic.RCS1037.severity = error +dotnet_diagnostic.RCS1055.severity = error +dotnet_diagnostic.RCS1062.severity = error +dotnet_diagnostic.RCS1066.severity = error +dotnet_diagnostic.RCS1069.severity = error +dotnet_diagnostic.RCS1071.severity = error +dotnet_diagnostic.RCS1074.severity = error +dotnet_diagnostic.RCS1090.severity = error +dotnet_diagnostic.RCS1138.severity = error +dotnet_diagnostic.RCS1139.severity = error +dotnet_diagnostic.RCS1163.severity = suggestion +dotnet_diagnostic.RCS1168.severity = suggestion +dotnet_diagnostic.RCS1188.severity = error +dotnet_diagnostic.RCS1201.severity = error +dotnet_diagnostic.RCS1207.severity = error +dotnet_diagnostic.RCS1211.severity = error +dotnet_diagnostic.RCS1507.severity = error + +dotnet_diagnostic.SA1000.severity = error +dotnet_diagnostic.SA1001.severity = error +dotnet_diagnostic.SA1002.severity = error +dotnet_diagnostic.SA1003.severity = error +dotnet_diagnostic.SA1004.severity = error +dotnet_diagnostic.SA1005.severity = error +dotnet_diagnostic.SA1006.severity = error +dotnet_diagnostic.SA1007.severity = error +dotnet_diagnostic.SA1008.severity = error +dotnet_diagnostic.SA1009.severity = error +dotnet_diagnostic.SA1010.severity = suggestion +dotnet_diagnostic.SA1011.severity = error +dotnet_diagnostic.SA1012.severity = error +dotnet_diagnostic.SA1013.severity = error +dotnet_diagnostic.SA1014.severity = error +dotnet_diagnostic.SA1015.severity = error +dotnet_diagnostic.SA1016.severity = error +dotnet_diagnostic.SA1017.severity = error +dotnet_diagnostic.SA1018.severity = error +dotnet_diagnostic.SA1019.severity = error +dotnet_diagnostic.SA1020.severity = error +dotnet_diagnostic.SA1021.severity = error +dotnet_diagnostic.SA1022.severity = error +dotnet_diagnostic.SA1023.severity = error +dotnet_diagnostic.SA1024.severity = error +dotnet_diagnostic.SA1025.severity = error +dotnet_diagnostic.SA1026.severity = error +dotnet_diagnostic.SA1027.severity = error +dotnet_diagnostic.SA1028.severity = error +dotnet_diagnostic.SA1100.severity = error +dotnet_diagnostic.SA1101.severity = none +dotnet_diagnostic.SA1102.severity = error +dotnet_diagnostic.SA1103.severity = error +dotnet_diagnostic.SA1104.severity = error +dotnet_diagnostic.SA1105.severity = error +dotnet_diagnostic.SA1106.severity = error +dotnet_diagnostic.SA1107.severity = error +dotnet_diagnostic.SA1108.severity = error +dotnet_diagnostic.SA1110.severity = error +dotnet_diagnostic.SA1111.severity = error +dotnet_diagnostic.SA1112.severity = error +dotnet_diagnostic.SA1113.severity = error +dotnet_diagnostic.SA1114.severity = error +dotnet_diagnostic.SA1115.severity = error +dotnet_diagnostic.SA1116.severity = error +dotnet_diagnostic.SA1117.severity = error +dotnet_diagnostic.SA1118.severity = error +dotnet_diagnostic.SA1119.severity = error +dotnet_diagnostic.SA1120.severity = error +dotnet_diagnostic.SA1121.severity = error +dotnet_diagnostic.SA1122.severity = error +dotnet_diagnostic.SA1123.severity = error +dotnet_diagnostic.SA1124.severity = error +dotnet_diagnostic.SA1125.severity = error +dotnet_diagnostic.SA1127.severity = error +dotnet_diagnostic.SA1128.severity = error +dotnet_diagnostic.SA1129.severity = error +dotnet_diagnostic.SA1130.severity = error +dotnet_diagnostic.SA1131.severity = error +dotnet_diagnostic.SA1132.severity = error +dotnet_diagnostic.SA1133.severity = error +dotnet_diagnostic.SA1134.severity = error +dotnet_diagnostic.SA1135.severity = error +dotnet_diagnostic.SA1136.severity = error +dotnet_diagnostic.SA1137.severity = error +dotnet_diagnostic.SA1139.severity = error +dotnet_diagnostic.SA1200.severity = none +dotnet_diagnostic.SA1201.severity = error +dotnet_diagnostic.SA1202.severity = error +dotnet_diagnostic.SA1203.severity = error +dotnet_diagnostic.SA1204.severity = error +dotnet_diagnostic.SA1205.severity = error +dotnet_diagnostic.SA1206.severity = error +dotnet_diagnostic.SA1207.severity = error +dotnet_diagnostic.SA1208.severity = error +dotnet_diagnostic.SA1209.severity = error +dotnet_diagnostic.SA1210.severity = error +dotnet_diagnostic.SA1211.severity = error +dotnet_diagnostic.SA1212.severity = error +dotnet_diagnostic.SA1213.severity = error +dotnet_diagnostic.SA1214.severity = error +dotnet_diagnostic.SA1216.severity = error +dotnet_diagnostic.SA1217.severity = error +dotnet_diagnostic.SA1300.severity = error +dotnet_diagnostic.SA1302.severity = error +dotnet_diagnostic.SA1303.severity = error +dotnet_diagnostic.SA1304.severity = error +dotnet_diagnostic.SA1306.severity = none +dotnet_diagnostic.SA1307.severity = error +dotnet_diagnostic.SA1308.severity = error +dotnet_diagnostic.SA1309.severity = none +dotnet_diagnostic.SA1310.severity = error +dotnet_diagnostic.SA1311.severity = none +dotnet_diagnostic.SA1312.severity = error +dotnet_diagnostic.SA1313.severity = error +dotnet_diagnostic.SA1314.severity = error +dotnet_diagnostic.SA1316.severity = none +dotnet_diagnostic.SA1400.severity = error +dotnet_diagnostic.SA1401.severity = error +dotnet_diagnostic.SA1402.severity = error +dotnet_diagnostic.SA1403.severity = error +dotnet_diagnostic.SA1404.severity = error +dotnet_diagnostic.SA1405.severity = error +dotnet_diagnostic.SA1406.severity = error +dotnet_diagnostic.SA1407.severity = error +dotnet_diagnostic.SA1408.severity = error +dotnet_diagnostic.SA1410.severity = error +dotnet_diagnostic.SA1411.severity = error +dotnet_diagnostic.SA1413.severity = none +dotnet_diagnostic.SA1500.severity = error +dotnet_diagnostic.SA1501.severity = error +dotnet_diagnostic.SA1502.severity = error +dotnet_diagnostic.SA1503.severity = error +dotnet_diagnostic.SA1504.severity = error +dotnet_diagnostic.SA1505.severity = none +dotnet_diagnostic.SA1506.severity = error +dotnet_diagnostic.SA1507.severity = error +dotnet_diagnostic.SA1508.severity = error +dotnet_diagnostic.SA1509.severity = error +dotnet_diagnostic.SA1510.severity = error +dotnet_diagnostic.SA1511.severity = error +dotnet_diagnostic.SA1512.severity = error +dotnet_diagnostic.SA1513.severity = error +dotnet_diagnostic.SA1514.severity = none +dotnet_diagnostic.SA1515.severity = error +dotnet_diagnostic.SA1516.severity = error +dotnet_diagnostic.SA1517.severity = error +dotnet_diagnostic.SA1518.severity = error +dotnet_diagnostic.SA1519.severity = error +dotnet_diagnostic.SA1520.severity = error +dotnet_diagnostic.SA1600.severity = error +dotnet_diagnostic.SA1601.severity = error +dotnet_diagnostic.SA1602.severity = error +dotnet_diagnostic.SA1604.severity = error +dotnet_diagnostic.SA1605.severity = error +dotnet_diagnostic.SA1606.severity = error +dotnet_diagnostic.SA1607.severity = error +dotnet_diagnostic.SA1608.severity = error +dotnet_diagnostic.SA1610.severity = error +dotnet_diagnostic.SA1611.severity = error +dotnet_diagnostic.SA1612.severity = error +dotnet_diagnostic.SA1613.severity = error +dotnet_diagnostic.SA1614.severity = error +dotnet_diagnostic.SA1615.severity = error +dotnet_diagnostic.SA1616.severity = error +dotnet_diagnostic.SA1617.severity = error +dotnet_diagnostic.SA1618.severity = error +dotnet_diagnostic.SA1619.severity = error +dotnet_diagnostic.SA1620.severity = error +dotnet_diagnostic.SA1621.severity = error +dotnet_diagnostic.SA1622.severity = error +dotnet_diagnostic.SA1623.severity = error +dotnet_diagnostic.SA1624.severity = error +dotnet_diagnostic.SA1625.severity = error +dotnet_diagnostic.SA1626.severity = error +dotnet_diagnostic.SA1627.severity = error +dotnet_diagnostic.SA1629.severity = error +dotnet_diagnostic.SA1633.severity = error +dotnet_diagnostic.SA1634.severity = error +dotnet_diagnostic.SA1635.severity = error +dotnet_diagnostic.SA1636.severity = error +dotnet_diagnostic.SA1637.severity = none +dotnet_diagnostic.SA1638.severity = none +dotnet_diagnostic.SA1640.severity = error +dotnet_diagnostic.SA1641.severity = error +dotnet_diagnostic.SA1642.severity = error +dotnet_diagnostic.SA1643.severity = error +dotnet_diagnostic.SA1649.severity = error +dotnet_diagnostic.SA1651.severity = error + +dotnet_diagnostic.SX1101.severity = error +dotnet_diagnostic.SX1309.severity = error +dotnet_diagnostic.SX1623.severity = none +dotnet_diagnostic.RCS1102.severity=error +dotnet_diagnostic.RCS1166.severity=error +dotnet_diagnostic.RCS1078i.severity=error +dotnet_diagnostic.RCS1248.severity=error +dotnet_diagnostic.RCS1080.severity=error +dotnet_diagnostic.RCS1077.severity=error +dotnet_diagnostic.CA1825.severity=error +dotnet_diagnostic.CA1812.severity=error +dotnet_diagnostic.CA1805.severity=error +dotnet_diagnostic.RCS1197.severity=error +dotnet_diagnostic.RCS1198.severity=suggestion +dotnet_diagnostic.RCS1231.severity=suggestion +dotnet_diagnostic.RCS1235.severity=error +dotnet_diagnostic.RCS1242.severity=error +dotnet_diagnostic.CA2016.severity=warning +dotnet_diagnostic.CA2014.severity=error +dotnet_diagnostic.RCS1010.severity=error +dotnet_diagnostic.RCS1006.severity=error +dotnet_diagnostic.RCS1005.severity=error +dotnet_diagnostic.RCS1020.severity=error +dotnet_diagnostic.RCS1049.severity=warning +dotnet_diagnostic.RCS1058.severity=warning +dotnet_diagnostic.RCS1068.severity=warning +dotnet_diagnostic.RCS1073.severity=warning +dotnet_diagnostic.RCS1084.severity=error +dotnet_diagnostic.RCS1085.severity=error +dotnet_diagnostic.RCS1105.severity=error +dotnet_diagnostic.RCS1112.severity=error +dotnet_diagnostic.RCS1128.severity=error +dotnet_diagnostic.RCS1143.severity=error +dotnet_diagnostic.RCS1171.severity=error +dotnet_diagnostic.RCS1173.severity=error +dotnet_diagnostic.RCS1176.severity=error +dotnet_diagnostic.RCS1177.severity=error +dotnet_diagnostic.RCS1179.severity=error +dotnet_diagnostic.RCS1180.severity=warning +dotnet_diagnostic.RCS1190.severity=error +dotnet_diagnostic.RCS1195.severity=error +dotnet_diagnostic.RCS1214.severity=error + # C++ Files [*.{cpp,h,in}] curly_bracket_next_line = true @@ -183,3 +525,5 @@ indent_size = 2 end_of_line = lf [*.{cmd, bat}] end_of_line = crlf + +vsspell_dictionary_languages = en-US \ No newline at end of file diff --git a/src/ReactiveUI.Validation.AndroidSupport/Extensions/ViewForExtensions.cs b/src/ReactiveUI.Validation.AndroidSupport/Extensions/ViewForExtensions.cs deleted file mode 100644 index adb33710..00000000 --- a/src/ReactiveUI.Validation.AndroidSupport/Extensions/ViewForExtensions.cs +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (c) 2022 .NET Foundation and Contributors. All rights reserved. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for full license information. - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Linq.Expressions; -using Android.Support.Design.Widget; -using ReactiveUI.Validation.Abstractions; -using ReactiveUI.Validation.Formatters; -using ReactiveUI.Validation.Formatters.Abstractions; -using ReactiveUI.Validation.Helpers; -using ReactiveUI.Validation.ValidationBindings; -using Splat; - -// ReSharper disable once CheckNamespace -namespace ReactiveUI.Validation.Extensions; - -/// -/// Android specific extensions methods associated to instances. -/// -[SuppressMessage("Roslynator", "RCS1163", Justification = "Needed for Expression context.")] -public static class ViewForExtensions -{ - /// - /// Platform binding to the TextInputLayout. - /// - /// IViewFor of TViewModel. - /// ViewModel type. - /// ViewModel property type. - /// IViewFor instance. - /// ViewModel instance. Can be null, used for generic type resolution. - /// ViewModel property. - /// View property to bind the validation message. - /// - /// Validation formatter. Defaults to . In order to override the global - /// default value, implement and register an instance of - /// IValidationTextFormatter<string> into Splat.Locator. - /// - /// Returns a object. - [SuppressMessage("Design", "CA1801: Parameter unused", Justification = "Used for generic resolution.")] - public static IDisposable BindValidation( - this TView view, - TViewModel? viewModel, - Expression> viewModelProperty, - TextInputLayout viewProperty, - IValidationTextFormatter? formatter = null) - where TView : IViewFor - where TViewModel : class, IReactiveObject, IValidatableViewModel - { - if (view is null) - { - throw new ArgumentNullException(nameof(view)); - } - - if (viewModelProperty is null) - { - throw new ArgumentNullException(nameof(viewModelProperty)); - } - - if (viewProperty is null) - { - throw new ArgumentNullException(nameof(viewProperty)); - } - - formatter ??= Locator.Current.GetService>() ?? - SingleLineFormatter.Default; - - return ValidationBinding.ForProperty( - view, - viewModelProperty, - (_, errors) => viewProperty.Error = errors.FirstOrDefault(msg => !string.IsNullOrEmpty(msg)), - formatter); - } - - /// - /// Platform binding to the TextInputLayout. - /// - /// Supports multiple validations for the same property. - /// IViewFor of TViewModel. - /// ViewModel type. - /// ViewModel property type. - /// IViewFor instance. - /// ViewModel instance. Can be null, used for generic type resolution. - /// ViewModel property. - /// View property to bind the validation message. - /// - /// Validation formatter. Defaults to . In order to override the global - /// default value, implement and register an instance of - /// IValidationTextFormatter<string> into Splat.Locator. - /// - /// Returns a object. - [ExcludeFromCodeCoverage] - [Obsolete("This method is no longer required, BindValidation now supports multiple validations.")] - [SuppressMessage("Design", "CA1801: Parameter unused", Justification = "Used for generic resolution.")] - public static IDisposable BindValidationEx( - this TView view, - TViewModel? viewModel, - Expression> viewModelProperty, - TextInputLayout viewProperty, - IValidationTextFormatter? formatter = null) - where TView : IViewFor - where TViewModel : class, IReactiveObject, IValidatableViewModel - { - if (view is null) - { - throw new ArgumentNullException(nameof(view)); - } - - if (viewModelProperty is null) - { - throw new ArgumentNullException(nameof(viewModelProperty)); - } - - if (viewProperty is null) - { - throw new ArgumentNullException(nameof(viewProperty)); - } - - formatter ??= Locator.Current.GetService>() ?? - SingleLineFormatter.Default; - - return ValidationBinding.ForProperty( - view, - viewModelProperty, - (_, errors) => viewProperty.Error = errors.FirstOrDefault(msg => !string.IsNullOrEmpty(msg)), - formatter); - } - - /// - /// Platform binding to the TextInputLayout. - /// - /// IViewFor of TViewModel. - /// ViewModel type. - /// IViewFor instance. - /// ViewModel instance. Can be null, used for generic type resolution. - /// ViewModel's ValidationHelper property. - /// View property to bind the validation message. - /// - /// Validation formatter. Defaults to . In order to override the global - /// default value, implement and register an instance of - /// IValidationTextFormatter<string> into Splat.Locator. - /// - /// Returns a object. - [SuppressMessage("Design", "CA1801: Parameter unused", Justification = "Used for generic resolution.")] - public static IDisposable BindValidation( - this TView view, - TViewModel? viewModel, - Expression> viewModelHelperProperty, - TextInputLayout viewProperty, - IValidationTextFormatter? formatter = null) - where TView : IViewFor - where TViewModel : class, IReactiveObject, IValidatableViewModel - { - if (view is null) - { - throw new ArgumentNullException(nameof(view)); - } - - if (viewModelHelperProperty is null) - { - throw new ArgumentNullException(nameof(viewModelHelperProperty)); - } - - if (viewProperty is null) - { - throw new ArgumentNullException(nameof(viewProperty)); - } - - formatter ??= Locator.Current.GetService>() ?? - SingleLineFormatter.Default; - - return ValidationBinding.ForValidationHelperProperty( - view, - viewModelHelperProperty, - (_, errorText) => viewProperty.Error = errorText, - formatter); - } -} diff --git a/src/ReactiveUI.Validation.AndroidSupport/ReactiveUI.Validation.AndroidSupport.csproj b/src/ReactiveUI.Validation.AndroidSupport/ReactiveUI.Validation.AndroidSupport.csproj deleted file mode 100644 index e1411a34..00000000 --- a/src/ReactiveUI.Validation.AndroidSupport/ReactiveUI.Validation.AndroidSupport.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - MonoAndroid13.0 - Provides ReactiveUI.Validation extensions for the Android Support Library - ReactiveUI.Validation.AndroidSupport - $(NoWarn);CS1591 - enable - - - - - - - - - - - diff --git a/src/ReactiveUI.Validation.AndroidX/Extensions/ViewForExtensions.cs b/src/ReactiveUI.Validation.AndroidX/Extensions/ViewForExtensions.cs deleted file mode 100644 index a899fca7..00000000 --- a/src/ReactiveUI.Validation.AndroidX/Extensions/ViewForExtensions.cs +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (c) 2022 .NET Foundation and Contributors. All rights reserved. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for full license information. - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Linq.Expressions; -using Google.Android.Material.TextField; -using ReactiveUI.Validation.Abstractions; -using ReactiveUI.Validation.Formatters; -using ReactiveUI.Validation.Formatters.Abstractions; -using ReactiveUI.Validation.Helpers; -using ReactiveUI.Validation.ValidationBindings; -using Splat; - -// ReSharper disable once CheckNamespace -namespace ReactiveUI.Validation.Extensions; - -/// -/// Android specific extensions methods associated to instances. -/// -[SuppressMessage("Roslynator", "RCS1163", Justification = "Needed for Expression context.")] -public static class ViewForExtensions -{ - /// - /// Platform binding to the TextInputLayout. - /// - /// IViewFor of TViewModel. - /// ViewModel type. - /// ViewModel property type. - /// IViewFor instance. - /// ViewModel instance. Can be null, used for generic type resolution. - /// ViewModel property. - /// View property to bind the validation message. - /// - /// Validation formatter. Defaults to . In order to override the global - /// default value, implement and register an instance of - /// IValidationTextFormatter<string> into Splat.Locator. - /// - /// Returns a object. - [SuppressMessage("Design", "CA1801: Parameter unused", Justification = "Used for generic resolution.")] - public static IDisposable BindValidation( - this TView view, - TViewModel? viewModel, - Expression> viewModelProperty, - TextInputLayout viewProperty, - IValidationTextFormatter? formatter = null) - where TView : IViewFor - where TViewModel : class, IReactiveObject, IValidatableViewModel - { - if (view is null) - { - throw new ArgumentNullException(nameof(view)); - } - - if (viewModelProperty is null) - { - throw new ArgumentNullException(nameof(viewModelProperty)); - } - - if (viewProperty is null) - { - throw new ArgumentNullException(nameof(viewProperty)); - } - - formatter ??= Locator.Current.GetService>() ?? - SingleLineFormatter.Default; - - return ValidationBinding.ForProperty( - view, - viewModelProperty, - (_, errors) => viewProperty.Error = errors.FirstOrDefault(msg => !string.IsNullOrEmpty(msg)), - formatter); - } - - /// - /// Platform binding to the TextInputLayout. - /// - /// Supports multiple validations for the same property. - /// IViewFor of TViewModel. - /// ViewModel type. - /// ViewModel property type. - /// IViewFor instance. - /// ViewModel instance. Can be null, used for generic type resolution. - /// ViewModel property. - /// View property to bind the validation message. - /// - /// Validation formatter. Defaults to . In order to override the global - /// default value, implement and register an instance of - /// IValidationTextFormatter<string> into Splat.Locator. - /// - /// Returns a object. - [ExcludeFromCodeCoverage] - [Obsolete("This method is no longer required, BindValidation now supports multiple validations.")] - [SuppressMessage("Design", "CA1801: Parameter unused", Justification = "Used for generic resolution.")] - public static IDisposable BindValidationEx( - this TView view, - TViewModel? viewModel, - Expression> viewModelProperty, - TextInputLayout viewProperty, - IValidationTextFormatter? formatter = null) - where TView : IViewFor - where TViewModel : class, IReactiveObject, IValidatableViewModel - { - if (view is null) - { - throw new ArgumentNullException(nameof(view)); - } - - if (viewModelProperty is null) - { - throw new ArgumentNullException(nameof(viewModelProperty)); - } - - if (viewProperty is null) - { - throw new ArgumentNullException(nameof(viewProperty)); - } - - formatter ??= Locator.Current.GetService>() ?? - SingleLineFormatter.Default; - - return ValidationBinding.ForProperty( - view, - viewModelProperty, - (_, errors) => viewProperty.Error = errors.FirstOrDefault(msg => !string.IsNullOrEmpty(msg)), - formatter); - } - - /// - /// Platform binding to the TextInputLayout. - /// - /// IViewFor of TViewModel. - /// ViewModel type. - /// IViewFor instance. - /// ViewModel instance. Can be null, used for generic type resolution. - /// ViewModel's ValidationHelper property. - /// View property to bind the validation message. - /// - /// Validation formatter. Defaults to . In order to override the global - /// default value, implement and register an instance of - /// IValidationTextFormatter<string> into Splat.Locator. - /// - /// Returns a object. - [SuppressMessage("Design", "CA1801: Parameter unused", Justification = "Used for generic resolution.")] - public static IDisposable BindValidation( - this TView view, - TViewModel? viewModel, - Expression> viewModelHelperProperty, - TextInputLayout viewProperty, - IValidationTextFormatter? formatter = null) - where TView : IViewFor - where TViewModel : class, IReactiveObject, IValidatableViewModel - { - if (view is null) - { - throw new ArgumentNullException(nameof(view)); - } - - if (viewModelHelperProperty is null) - { - throw new ArgumentNullException(nameof(viewModelHelperProperty)); - } - - if (viewProperty is null) - { - throw new ArgumentNullException(nameof(viewProperty)); - } - - formatter ??= Locator.Current.GetService>() ?? - SingleLineFormatter.Default; - - return ValidationBinding.ForValidationHelperProperty( - view, - viewModelHelperProperty, - (_, errorText) => viewProperty.Error = errorText, - formatter); - } -} \ No newline at end of file diff --git a/src/ReactiveUI.Validation.AndroidX/ReactiveUI.Validation.AndroidX.csproj b/src/ReactiveUI.Validation.AndroidX/ReactiveUI.Validation.AndroidX.csproj deleted file mode 100644 index 842b9058..00000000 --- a/src/ReactiveUI.Validation.AndroidX/ReactiveUI.Validation.AndroidX.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - MonoAndroid13.0;net7.0-android;net8.0-android - Provides ReactiveUI.Validation extensions for the AndroidX Library - ReactiveUI.Validation.AndroidX - $(NoWarn);CS1591 - enable - - - - - - - - - - diff --git a/src/ReactiveUI.Validation.Tests/API/ApiApprovalTests.ValidationProject.DotNet6_0.verified.txt b/src/ReactiveUI.Validation.Tests/API/ApiApprovalTests.ValidationProject.DotNet6_0.verified.txt index 06bed432..a8eda0db 100644 --- a/src/ReactiveUI.Validation.Tests/API/ApiApprovalTests.ValidationProject.DotNet6_0.verified.txt +++ b/src/ReactiveUI.Validation.Tests/API/ApiApprovalTests.ValidationProject.DotNet6_0.verified.txt @@ -3,7 +3,7 @@ namespace ReactiveUI.Validation.Abstractions { public interface IValidatableViewModel { - ReactiveUI.Validation.Contexts.ValidationContext ValidationContext { get; } + ReactiveUI.Validation.Contexts.IValidationContext ValidationContext { get; } } } namespace ReactiveUI.Validation.Collections @@ -106,14 +106,24 @@ namespace ReactiveUI.Validation.Components } namespace ReactiveUI.Validation.Contexts { - public class ValidationContext : ReactiveUI.ReactiveObject, ReactiveUI.Validation.Components.Abstractions.IValidationComponent, System.IDisposable + public interface IValidationContext : ReactiveUI.IReactiveObject, ReactiveUI.Validation.Components.Abstractions.IValidationComponent, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging, System.IDisposable, System.Reactive.Disposables.ICancelable + { + System.IObservable Valid { get; } + DynamicData.IObservableList Validations { get; } + void Add(ReactiveUI.Validation.Components.Abstractions.IValidationComponent validation); + bool GetIsValid(); + void Remove(ReactiveUI.Validation.Components.Abstractions.IValidationComponent validation); + void RemoveMany(System.Collections.Generic.IEnumerable validations); + } + public class ValidationContext : ReactiveUI.ReactiveObject, ReactiveUI.IReactiveObject, ReactiveUI.Validation.Components.Abstractions.IValidationComponent, ReactiveUI.Validation.Contexts.IValidationContext, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging, System.IDisposable, System.Reactive.Disposables.ICancelable { public ValidationContext(System.Reactive.Concurrency.IScheduler? scheduler = null) { } + public bool IsDisposed { get; } public bool IsValid { get; } public ReactiveUI.Validation.Collections.IValidationText Text { get; } public System.IObservable Valid { get; } public System.IObservable ValidationStatusChange { get; } - public System.Collections.ObjectModel.ReadOnlyObservableCollection Validations { get; } + public DynamicData.IObservableList Validations { get; } public void Add(ReactiveUI.Validation.Components.Abstractions.IValidationComponent validation) { } public void Dispose() { } protected virtual void Dispose(bool disposing) { } @@ -161,7 +171,7 @@ namespace ReactiveUI.Validation.Extensions } public static class ValidationContextExtensions { - public static System.IObservable> ObserveFor(this ReactiveUI.Validation.Contexts.ValidationContext context, System.Linq.Expressions.Expression> viewModelProperty, bool strict = true) { } + public static System.IObservable> ObserveFor(this ReactiveUI.Validation.Contexts.IValidationContext context, System.Linq.Expressions.Expression> viewModelProperty, bool strict = true) { } } public static class ViewForExtensions { @@ -194,12 +204,14 @@ namespace ReactiveUI.Validation.Formatters } namespace ReactiveUI.Validation.Helpers { - public abstract class ReactiveValidationObject : ReactiveUI.ReactiveObject, ReactiveUI.Validation.Abstractions.IValidatableViewModel, System.ComponentModel.INotifyDataErrorInfo + public abstract class ReactiveValidationObject : ReactiveUI.ReactiveObject, ReactiveUI.Validation.Abstractions.IValidatableViewModel, System.ComponentModel.INotifyDataErrorInfo, System.IDisposable { protected ReactiveValidationObject(System.Reactive.Concurrency.IScheduler? scheduler = null, ReactiveUI.Validation.Formatters.Abstractions.IValidationTextFormatter? formatter = null) { } public bool HasErrors { get; } - public ReactiveUI.Validation.Contexts.ValidationContext ValidationContext { get; } + public ReactiveUI.Validation.Contexts.IValidationContext ValidationContext { get; } public event System.EventHandler? ErrorsChanged; + public void Dispose() { } + protected virtual void Dispose(bool disposing) { } public virtual System.Collections.IEnumerable GetErrors(string? propertyName) { } protected void RaiseErrorsChanged(string propertyName = "") { } } @@ -207,7 +219,7 @@ namespace ReactiveUI.Validation.Helpers { public ValidationHelper(ReactiveUI.Validation.Components.Abstractions.IValidationComponent validation, System.IDisposable? cleanup = null) { } public bool IsValid { get; } - public ReactiveUI.Validation.Collections.IValidationText? Message { get; } + public ReactiveUI.Validation.Collections.IValidationText Message { get; } public System.IObservable ValidationChanged { get; } public void Dispose() { } protected virtual void Dispose(bool disposing) { } diff --git a/src/ReactiveUI.Validation.Tests/API/ApiApprovalTests.ValidationProject.DotNet7_0.verified.txt b/src/ReactiveUI.Validation.Tests/API/ApiApprovalTests.ValidationProject.DotNet7_0.verified.txt index 3db0ad85..7e449e12 100644 --- a/src/ReactiveUI.Validation.Tests/API/ApiApprovalTests.ValidationProject.DotNet7_0.verified.txt +++ b/src/ReactiveUI.Validation.Tests/API/ApiApprovalTests.ValidationProject.DotNet7_0.verified.txt @@ -3,7 +3,7 @@ namespace ReactiveUI.Validation.Abstractions { public interface IValidatableViewModel { - ReactiveUI.Validation.Contexts.ValidationContext ValidationContext { get; } + ReactiveUI.Validation.Contexts.IValidationContext ValidationContext { get; } } } namespace ReactiveUI.Validation.Collections @@ -106,14 +106,24 @@ namespace ReactiveUI.Validation.Components } namespace ReactiveUI.Validation.Contexts { - public class ValidationContext : ReactiveUI.ReactiveObject, ReactiveUI.Validation.Components.Abstractions.IValidationComponent, System.IDisposable + public interface IValidationContext : ReactiveUI.IReactiveObject, ReactiveUI.Validation.Components.Abstractions.IValidationComponent, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging, System.IDisposable, System.Reactive.Disposables.ICancelable + { + System.IObservable Valid { get; } + DynamicData.IObservableList Validations { get; } + void Add(ReactiveUI.Validation.Components.Abstractions.IValidationComponent validation); + bool GetIsValid(); + void Remove(ReactiveUI.Validation.Components.Abstractions.IValidationComponent validation); + void RemoveMany(System.Collections.Generic.IEnumerable validations); + } + public class ValidationContext : ReactiveUI.ReactiveObject, ReactiveUI.IReactiveObject, ReactiveUI.Validation.Components.Abstractions.IValidationComponent, ReactiveUI.Validation.Contexts.IValidationContext, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging, System.IDisposable, System.Reactive.Disposables.ICancelable { public ValidationContext(System.Reactive.Concurrency.IScheduler? scheduler = null) { } + public bool IsDisposed { get; } public bool IsValid { get; } public ReactiveUI.Validation.Collections.IValidationText Text { get; } public System.IObservable Valid { get; } public System.IObservable ValidationStatusChange { get; } - public System.Collections.ObjectModel.ReadOnlyObservableCollection Validations { get; } + public DynamicData.IObservableList Validations { get; } public void Add(ReactiveUI.Validation.Components.Abstractions.IValidationComponent validation) { } public void Dispose() { } protected virtual void Dispose(bool disposing) { } @@ -161,7 +171,7 @@ namespace ReactiveUI.Validation.Extensions } public static class ValidationContextExtensions { - public static System.IObservable> ObserveFor(this ReactiveUI.Validation.Contexts.ValidationContext context, System.Linq.Expressions.Expression> viewModelProperty, bool strict = true) { } + public static System.IObservable> ObserveFor(this ReactiveUI.Validation.Contexts.IValidationContext context, System.Linq.Expressions.Expression> viewModelProperty, bool strict = true) { } } public static class ViewForExtensions { @@ -194,12 +204,14 @@ namespace ReactiveUI.Validation.Formatters } namespace ReactiveUI.Validation.Helpers { - public abstract class ReactiveValidationObject : ReactiveUI.ReactiveObject, ReactiveUI.Validation.Abstractions.IValidatableViewModel, System.ComponentModel.INotifyDataErrorInfo + public abstract class ReactiveValidationObject : ReactiveUI.ReactiveObject, ReactiveUI.Validation.Abstractions.IValidatableViewModel, System.ComponentModel.INotifyDataErrorInfo, System.IDisposable { protected ReactiveValidationObject(System.Reactive.Concurrency.IScheduler? scheduler = null, ReactiveUI.Validation.Formatters.Abstractions.IValidationTextFormatter? formatter = null) { } public bool HasErrors { get; } - public ReactiveUI.Validation.Contexts.ValidationContext ValidationContext { get; } + public ReactiveUI.Validation.Contexts.IValidationContext ValidationContext { get; } public event System.EventHandler? ErrorsChanged; + public void Dispose() { } + protected virtual void Dispose(bool disposing) { } public virtual System.Collections.IEnumerable GetErrors(string? propertyName) { } protected void RaiseErrorsChanged(string propertyName = "") { } } @@ -207,7 +219,7 @@ namespace ReactiveUI.Validation.Helpers { public ValidationHelper(ReactiveUI.Validation.Components.Abstractions.IValidationComponent validation, System.IDisposable? cleanup = null) { } public bool IsValid { get; } - public ReactiveUI.Validation.Collections.IValidationText? Message { get; } + public ReactiveUI.Validation.Collections.IValidationText Message { get; } public System.IObservable ValidationChanged { get; } public void Dispose() { } protected virtual void Dispose(bool disposing) { } diff --git a/src/ReactiveUI.Validation.Tests/API/ApiApprovalTests.ValidationProject.DotNet8_0.verified.txt b/src/ReactiveUI.Validation.Tests/API/ApiApprovalTests.ValidationProject.DotNet8_0.verified.txt index 548278ff..ad7c7130 100644 --- a/src/ReactiveUI.Validation.Tests/API/ApiApprovalTests.ValidationProject.DotNet8_0.verified.txt +++ b/src/ReactiveUI.Validation.Tests/API/ApiApprovalTests.ValidationProject.DotNet8_0.verified.txt @@ -3,7 +3,7 @@ namespace ReactiveUI.Validation.Abstractions { public interface IValidatableViewModel { - ReactiveUI.Validation.Contexts.ValidationContext ValidationContext { get; } + ReactiveUI.Validation.Contexts.IValidationContext ValidationContext { get; } } } namespace ReactiveUI.Validation.Collections @@ -106,14 +106,24 @@ namespace ReactiveUI.Validation.Components } namespace ReactiveUI.Validation.Contexts { - public class ValidationContext : ReactiveUI.ReactiveObject, ReactiveUI.Validation.Components.Abstractions.IValidationComponent, System.IDisposable + public interface IValidationContext : ReactiveUI.IReactiveObject, ReactiveUI.Validation.Components.Abstractions.IValidationComponent, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging, System.IDisposable, System.Reactive.Disposables.ICancelable + { + System.IObservable Valid { get; } + DynamicData.IObservableList Validations { get; } + void Add(ReactiveUI.Validation.Components.Abstractions.IValidationComponent validation); + bool GetIsValid(); + void Remove(ReactiveUI.Validation.Components.Abstractions.IValidationComponent validation); + void RemoveMany(System.Collections.Generic.IEnumerable validations); + } + public class ValidationContext : ReactiveUI.ReactiveObject, ReactiveUI.IReactiveObject, ReactiveUI.Validation.Components.Abstractions.IValidationComponent, ReactiveUI.Validation.Contexts.IValidationContext, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging, System.IDisposable, System.Reactive.Disposables.ICancelable { public ValidationContext(System.Reactive.Concurrency.IScheduler? scheduler = null) { } + public bool IsDisposed { get; } public bool IsValid { get; } public ReactiveUI.Validation.Collections.IValidationText Text { get; } public System.IObservable Valid { get; } public System.IObservable ValidationStatusChange { get; } - public System.Collections.ObjectModel.ReadOnlyObservableCollection Validations { get; } + public DynamicData.IObservableList Validations { get; } public void Add(ReactiveUI.Validation.Components.Abstractions.IValidationComponent validation) { } public void Dispose() { } protected virtual void Dispose(bool disposing) { } @@ -161,7 +171,7 @@ namespace ReactiveUI.Validation.Extensions } public static class ValidationContextExtensions { - public static System.IObservable> ObserveFor(this ReactiveUI.Validation.Contexts.ValidationContext context, System.Linq.Expressions.Expression> viewModelProperty, bool strict = true) { } + public static System.IObservable> ObserveFor(this ReactiveUI.Validation.Contexts.IValidationContext context, System.Linq.Expressions.Expression> viewModelProperty, bool strict = true) { } } public static class ViewForExtensions { @@ -194,12 +204,14 @@ namespace ReactiveUI.Validation.Formatters } namespace ReactiveUI.Validation.Helpers { - public abstract class ReactiveValidationObject : ReactiveUI.ReactiveObject, ReactiveUI.Validation.Abstractions.IValidatableViewModel, System.ComponentModel.INotifyDataErrorInfo + public abstract class ReactiveValidationObject : ReactiveUI.ReactiveObject, ReactiveUI.Validation.Abstractions.IValidatableViewModel, System.ComponentModel.INotifyDataErrorInfo, System.IDisposable { protected ReactiveValidationObject(System.Reactive.Concurrency.IScheduler? scheduler = null, ReactiveUI.Validation.Formatters.Abstractions.IValidationTextFormatter? formatter = null) { } public bool HasErrors { get; } - public ReactiveUI.Validation.Contexts.ValidationContext ValidationContext { get; } + public ReactiveUI.Validation.Contexts.IValidationContext ValidationContext { get; } public event System.EventHandler? ErrorsChanged; + public void Dispose() { } + protected virtual void Dispose(bool disposing) { } public virtual System.Collections.IEnumerable GetErrors(string? propertyName) { } protected void RaiseErrorsChanged(string propertyName = "") { } } @@ -207,7 +219,7 @@ namespace ReactiveUI.Validation.Helpers { public ValidationHelper(ReactiveUI.Validation.Components.Abstractions.IValidationComponent validation, System.IDisposable? cleanup = null) { } public bool IsValid { get; } - public ReactiveUI.Validation.Collections.IValidationText? Message { get; } + public ReactiveUI.Validation.Collections.IValidationText Message { get; } public System.IObservable ValidationChanged { get; } public void Dispose() { } protected virtual void Dispose(bool disposing) { } diff --git a/src/ReactiveUI.Validation.Tests/MemoryLeakTests.cs b/src/ReactiveUI.Validation.Tests/MemoryLeakTests.cs new file mode 100644 index 00000000..ba48c583 --- /dev/null +++ b/src/ReactiveUI.Validation.Tests/MemoryLeakTests.cs @@ -0,0 +1,64 @@ +// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using FluentAssertions; +using JetBrains.dotMemoryUnit; +using ReactiveUI.Validation.Tests.Models; +using Xunit; +using Xunit.Abstractions; + +namespace ReactiveUI.Validation.Tests; + +/// +/// MemoryLeakTests. +/// +public class MemoryLeakTests +{ + /// + /// Initializes a new instance of the class. + /// + /// The test output helper. + public MemoryLeakTests(ITestOutputHelper testOutputHelper) + { + ArgumentNullException.ThrowIfNull(testOutputHelper); + + DotMemoryUnitTestOutput.SetOutputMethod(testOutputHelper.WriteLine); + } + + /// Tests whether the created object can be garbage collected. + [Fact] + [DotMemoryUnit(FailIfRunWithoutSupport = false)] + public void Instance_Released_IsGarbageCollected() + { + WeakReference reference = null; + new Action( + () => + { + var sut = new TestClassMemory(); + + reference = new WeakReference(sut, true); + sut.Dispose(); + })(); + + // Sut should have gone out of scope about now, so the garbage collector can clean it up + dotMemory.Check( + memory => memory.GetObjects( + where => where.Type.Is()).ObjectsCount.Should().Be(0, "it is out of scope")); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + + if (reference.Target is TestClassMemory sut) + { + // ReactiveObject does not inherit from IDisposable, so we need to check ValidationContext + sut.ValidationContext.Should().BeNull("it is garbage collected"); + } + else + { + reference.Target.Should().BeNull("it is garbage collected"); + } + } +} diff --git a/src/ReactiveUI.Validation.Tests/Models/SourceDestinationViewModel.cs b/src/ReactiveUI.Validation.Tests/Models/SourceDestinationViewModel.cs index 16f673b0..3189ed0d 100644 --- a/src/ReactiveUI.Validation.Tests/Models/SourceDestinationViewModel.cs +++ b/src/ReactiveUI.Validation.Tests/Models/SourceDestinationViewModel.cs @@ -36,5 +36,5 @@ public TestViewModel Destination } /// - public ValidationContext ValidationContext { get; } = new(Scheduler.Immediate); -} \ No newline at end of file + public IValidationContext ValidationContext { get; } = new ValidationContext(Scheduler.Immediate); +} diff --git a/src/ReactiveUI.Validation.Tests/Models/TestClassMemory.cs b/src/ReactiveUI.Validation.Tests/Models/TestClassMemory.cs new file mode 100644 index 00000000..fccca40f --- /dev/null +++ b/src/ReactiveUI.Validation.Tests/Models/TestClassMemory.cs @@ -0,0 +1,60 @@ +// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.Reactive.Disposables; +using ReactiveUI.Validation.Extensions; +using ReactiveUI.Validation.Helpers; + +namespace ReactiveUI.Validation.Tests.Models; + +/// +/// TestClassMemory. +/// +/// +/// +public sealed class TestClassMemory : ReactiveValidationObject +{ + private readonly CompositeDisposable _disposable = []; + + /// + /// Initializes a new instance of the class. + /// + public TestClassMemory() + { + this.ValidationRule( + vmp => vmp.Name, + name => !string.IsNullOrEmpty(name), + "The name is empty.") + .DisposeWith(_disposable); + + // commenting out the following statement makes the test green + ValidationContext.ValidationStatusChange + .Subscribe(/* you do something here, but this does not matter for now. */) + .DisposeWith(_disposable); + } + + /// + /// Gets or sets the name. + /// + /// + /// The name. + /// + public string Name { get; set; } + + /// + /// Disposes the specified disposing. + /// + /// if set to true [disposing]. + protected override void Dispose(bool disposing) + { + if (!_disposable.IsDisposed && disposing) + { + _disposable.Dispose(); + } + + base.Dispose(disposing); + } +} diff --git a/src/ReactiveUI.Validation.Tests/Models/TestViewModel.cs b/src/ReactiveUI.Validation.Tests/Models/TestViewModel.cs index c0af3336..cf6c9e46 100644 --- a/src/ReactiveUI.Validation.Tests/Models/TestViewModel.cs +++ b/src/ReactiveUI.Validation.Tests/Models/TestViewModel.cs @@ -3,7 +3,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System; using System.Reactive.Concurrency; +using System.Reactive.Disposables; using ReactiveUI.Validation.Abstractions; using ReactiveUI.Validation.Contexts; using ReactiveUI.Validation.Helpers; @@ -47,5 +49,5 @@ public ValidationHelper NameRule } /// - public ValidationContext ValidationContext { get; } = new(ImmediateScheduler.Instance); -} \ No newline at end of file + public IValidationContext ValidationContext { get; } = new ValidationContext(ImmediateScheduler.Instance); +} diff --git a/src/ReactiveUI.Validation.Tests/NotifyDataErrorInfoTests.cs b/src/ReactiveUI.Validation.Tests/NotifyDataErrorInfoTests.cs index f6249b4f..14ceef14 100644 --- a/src/ReactiveUI.Validation.Tests/NotifyDataErrorInfoTests.cs +++ b/src/ReactiveUI.Validation.Tests/NotifyDataErrorInfoTests.cs @@ -44,7 +44,7 @@ public void ShouldMarkPropertiesAsInvalidOnInit() // Verify validation context behavior. Assert.False(viewModel.ValidationContext.IsValid); - Assert.Single(viewModel.ValidationContext.Validations); + Assert.Single(viewModel.ValidationContext.Validations.Items); Assert.Equal(NameShouldNotBeEmptyMessage, view.NameErrorLabel); // Verify INotifyDataErrorInfo behavior. @@ -75,7 +75,7 @@ public void ShouldSynchronizeNotifyDataErrorInfoWithValidationContext() // Verify the initial state. Assert.True(viewModel.HasErrors); Assert.False(viewModel.ValidationContext.IsValid); - Assert.Single(viewModel.ValidationContext.Validations); + Assert.Single(viewModel.ValidationContext.Validations.Items); Assert.Equal(NameShouldNotBeEmptyMessage, viewModel.GetErrors("Name").Cast().First()); Assert.Equal(NameShouldNotBeEmptyMessage, view.NameErrorLabel); @@ -94,7 +94,7 @@ public void ShouldSynchronizeNotifyDataErrorInfoWithValidationContext() // Verify the changed state. Assert.True(viewModel.HasErrors); Assert.False(viewModel.ValidationContext.IsValid); - Assert.Single(viewModel.ValidationContext.Validations); + Assert.Single(viewModel.ValidationContext.Validations.Items); Assert.Equal(NameShouldNotBeEmptyMessage, viewModel.GetErrors("Name").Cast().First()); Assert.Equal(NameShouldNotBeEmptyMessage, view.NameErrorLabel); } @@ -120,7 +120,7 @@ public void ShouldFireErrorsChangedEventWhenValidationStateChanges() Assert.True(viewModel.HasErrors); Assert.False(viewModel.ValidationContext.IsValid); - Assert.Single(viewModel.ValidationContext.Validations); + Assert.Single(viewModel.ValidationContext.Validations.Items); Assert.Single(viewModel.GetErrors("Name").Cast()); viewModel.Name = "JoJo"; @@ -150,7 +150,7 @@ public void ShouldDeliverErrorsWhenModelObservableValidationTriggers() Assert.False(viewModel.HasErrors); Assert.True(viewModel.ValidationContext.IsValid); - Assert.Single(viewModel.ValidationContext.Validations); + Assert.Single(viewModel.ValidationContext.Validations.Items); Assert.Empty(viewModel.GetErrors(nameof(viewModel.Name)).Cast()); Assert.Empty(viewModel.GetErrors(nameof(viewModel.OtherName)).Cast()); diff --git a/src/ReactiveUI.Validation.Tests/ReactiveUI.Validation.Tests.csproj b/src/ReactiveUI.Validation.Tests/ReactiveUI.Validation.Tests.csproj index 1e87f729..5c61ac4c 100644 --- a/src/ReactiveUI.Validation.Tests/ReactiveUI.Validation.Tests.csproj +++ b/src/ReactiveUI.Validation.Tests/ReactiveUI.Validation.Tests.csproj @@ -22,7 +22,9 @@ all runtime; build; native; contentfiles; analyzers - + + + diff --git a/src/ReactiveUI.Validation.Tests/ValidationBindingTests.cs b/src/ReactiveUI.Validation.Tests/ValidationBindingTests.cs index 3424902d..22b113ea 100644 --- a/src/ReactiveUI.Validation.Tests/ValidationBindingTests.cs +++ b/src/ReactiveUI.Validation.Tests/ValidationBindingTests.cs @@ -106,7 +106,7 @@ public void RegisterValidationsWithDifferentLambdaNameWorksTest() view.BindValidation(view.ViewModel, vm => vm.Name, v => v.NameErrorLabel); Assert.True(view.ViewModel.ValidationContext.IsValid); - Assert.Single(view.ViewModel.ValidationContext.Validations); + Assert.Single(view.ViewModel.ValidationContext.Validations.Items); } /// @@ -210,7 +210,7 @@ public void ShouldSupportBindingValidationHelperProperties() view.BindValidation(view.ViewModel, vm => vm.NameRule, v => v.NameErrorLabel); Assert.False(view.ViewModel.ValidationContext.IsValid); - Assert.Single(view.ViewModel.ValidationContext.Validations); + Assert.Single(view.ViewModel.ValidationContext.Validations.Items); Assert.Equal(nameErrorMessage, view.NameErrorLabel); view.ViewModel.Name = "Jonathan"; @@ -249,14 +249,14 @@ public void ShouldSupportBindingModelObservableValidationHelperProperties() view.BindValidation(view.ViewModel, vm => vm.NameRule, v => v.NameErrorLabel); Assert.False(view.ViewModel.ValidationContext.IsValid); - Assert.Single(view.ViewModel.ValidationContext.Validations); + Assert.Single(view.ViewModel.ValidationContext.Validations.Items); Assert.Equal(namesShouldMatchMessage, view.NameErrorLabel); view.ViewModel.Name = "Bongo"; view.ViewModel.Name2 = "Bongo"; Assert.True(view.ViewModel.ValidationContext.IsValid); - Assert.Single(view.ViewModel.ValidationContext.Validations); + Assert.Single(view.ViewModel.ValidationContext.Validations.Items); Assert.Empty(view.NameErrorLabel); } @@ -779,25 +779,15 @@ public void ShouldSupportDelayedViewModelInitialization() Assert.Equal(errorMessage, view.NameErrorContainer.Text); } - private class CustomValidationState : IValidationState + private class CustomValidationState(bool isValid, string message) : IValidationState { - public CustomValidationState(bool isValid, string message) - { - IsValid = isValid; - Text = isValid ? ValidationText.Empty : ValidationText.Create(message); - } - - public IValidationText Text { get; } + public IValidationText Text { get; } = isValid ? ValidationText.Empty : ValidationText.Create(message); - public bool IsValid { get; } + public bool IsValid { get; } = isValid; } - private class ConstFormatter : IValidationTextFormatter + private class ConstFormatter(string text) : IValidationTextFormatter { - private readonly string _text; - - public ConstFormatter(string text) => _text = text; - - public string Format(IValidationText validationText) => _text; + public string Format(IValidationText validationText) => text; } } diff --git a/src/ReactiveUI.Validation.Tests/ValidationTextTests.cs b/src/ReactiveUI.Validation.Tests/ValidationTextTests.cs index d45dbb64..ac649c50 100644 --- a/src/ReactiveUI.Validation.Tests/ValidationTextTests.cs +++ b/src/ReactiveUI.Validation.Tests/ValidationTextTests.cs @@ -9,6 +9,7 @@ using ReactiveUI.Validation.Collections; using ReactiveUI.Validation.Contexts; using Xunit; +using Xunit.Abstractions; namespace ReactiveUI.Validation.Tests; @@ -23,14 +24,12 @@ public class ValidationTextTests [Fact] public void NoneValidationTextIsEmpty() { - IValidationText vt = ValidationText.None; + var vt = ValidationText.None; Assert.Equal(0, vt.Count); // Calling Count() checks the enumeration returns no results, unlike the Count property. -#pragma warning disable CA1829 // Use Length/Count property instead of Count() when available - Assert.Equal(0, vt.Count()); -#pragma warning restore CA1829 // Use Length/Count property instead of Count() when available + Assert.Equal(0, vt.Count); Assert.Equal(string.Empty, vt.ToSingleLine()); } @@ -40,14 +39,12 @@ public void NoneValidationTextIsEmpty() [Fact] public void EmptyValidationTextIsSingleEmpty() { - IValidationText vt = ValidationText.Empty; + var vt = ValidationText.Empty; Assert.Equal(1, vt.Count); // Calling Count() checks the enumeration returns no results, unlike the Count property. -#pragma warning disable CA1829 // Use Length/Count property instead of Count() when available - Assert.Equal(1, vt.Count()); -#pragma warning restore CA1829 // Use Length/Count property instead of Count() when available + Assert.Equal(1, vt.Count); Assert.Same(string.Empty, vt.Single()); Assert.Equal(string.Empty, vt.ToSingleLine()); } @@ -58,7 +55,7 @@ public void EmptyValidationTextIsSingleEmpty() [Fact] public void ParameterlessCreateReturnsNone() { - IValidationText vt = ValidationText.Create(); + var vt = ValidationText.Create(); Assert.Same(ValidationText.None, vt); } @@ -69,7 +66,7 @@ public void ParameterlessCreateReturnsNone() [Fact] public void CreateEmptyStringEnumerableReturnsNone() { - IValidationText vt = ValidationText.Create((IEnumerable)Array.Empty()); + var vt = ValidationText.Create((IEnumerable)[]); Assert.Same(ValidationText.None, vt); } @@ -80,7 +77,7 @@ public void CreateEmptyStringEnumerableReturnsNone() [Fact] public void CreateEmptyValidationTextEnumerableReturnsNone() { - IValidationText vt = ValidationText.Create(Array.Empty()); + var vt = ValidationText.Create(Array.Empty()); Assert.Same(ValidationText.None, vt); } @@ -91,7 +88,7 @@ public void CreateEmptyValidationTextEnumerableReturnsNone() [Fact] public void CreateNullReturnsNone() { - IValidationText vt = ValidationText.Create((string)null); + var vt = ValidationText.Create((string)null); Assert.Same(ValidationText.None, vt); } @@ -102,7 +99,7 @@ public void CreateNullReturnsNone() [Fact] public void CreateNullStringEnumerableReturnsNone() { - IValidationText vt = ValidationText.Create((IEnumerable)null); + var vt = ValidationText.Create((IEnumerable)null); Assert.Same(ValidationText.None, vt); } @@ -113,7 +110,7 @@ public void CreateNullStringEnumerableReturnsNone() [Fact] public void CreateNullValidationTextEnumerableReturnsNone() { - IValidationText vt = ValidationText.Create((IEnumerable)null); + var vt = ValidationText.Create((IEnumerable)null); Assert.Same(ValidationText.None, vt); } @@ -124,7 +121,7 @@ public void CreateNullValidationTextEnumerableReturnsNone() [Fact] public void CreateNullItemStringEnumerableReturnsNone() { - IValidationText vt = ValidationText.Create((IEnumerable)new string[] { null }); + var vt = ValidationText.Create((IEnumerable)[null]); Assert.Same(ValidationText.None, vt); } @@ -135,7 +132,7 @@ public void CreateNullItemStringEnumerableReturnsNone() [Fact] public void CreateNoneItemValidationTextEnumerableReturnsNone() { - IValidationText vt = ValidationText.Create(new[] { ValidationText.None }); + var vt = ValidationText.Create(new[] { ValidationText.None }); Assert.Same(ValidationText.None, vt); } @@ -146,7 +143,7 @@ public void CreateNoneItemValidationTextEnumerableReturnsNone() [Fact] public void CreateNoneItemStringEnumerableReturnsNone() { - IValidationText vt = ValidationText.Create(ValidationText.None); + var vt = ValidationText.Create(ValidationText.None); Assert.Same(ValidationText.None, vt); } @@ -157,7 +154,7 @@ public void CreateNoneItemStringEnumerableReturnsNone() [Fact] public void CreateStringEmptyReturnsEmpty() { - IValidationText vt = ValidationText.Create(string.Empty); + var vt = ValidationText.Create(string.Empty); Assert.Same(ValidationText.Empty, vt); } @@ -168,7 +165,7 @@ public void CreateStringEmptyReturnsEmpty() [Fact] public void CreateSingleStringEmptyReturnsEmpty() { - IValidationText vt = ValidationText.Create((IEnumerable)new[] { string.Empty }); + var vt = ValidationText.Create((IEnumerable)[string.Empty]); Assert.Same(ValidationText.Empty, vt); } @@ -179,7 +176,7 @@ public void CreateSingleStringEmptyReturnsEmpty() [Fact] public void CreateValidationTextEmptyReturnsEmpty() { - IValidationText vt = ValidationText.Create(new[] { ValidationText.Empty }); + var vt = ValidationText.Create(new[] { ValidationText.Empty }); Assert.Same(ValidationText.Empty, vt); } @@ -190,7 +187,7 @@ public void CreateValidationTextEmptyReturnsEmpty() [Fact] public void CombineValidationTextNoneReturnsNone() { - IValidationText vt = ValidationText.Create(new[] { ValidationText.None, ValidationText.None }); + var vt = ValidationText.Create(new[] { ValidationText.None, ValidationText.None }); Assert.Same(ValidationText.None, vt); } @@ -201,7 +198,7 @@ public void CombineValidationTextNoneReturnsNone() [Fact] public void CombineValidationTextEmptyAndNoneReturnsEmpty() { - IValidationText vt = ValidationText.Create(new[] { ValidationText.None, ValidationText.Empty }); + var vt = ValidationText.Create(new[] { ValidationText.None, ValidationText.Empty }); Assert.Same(ValidationText.Empty, vt); } @@ -213,15 +210,13 @@ public void CombineValidationTextEmptyAndNoneReturnsEmpty() [Fact] public void CombineValidationTextEmptyReturnsTwoEmpty() { - IValidationText vt = ValidationText.Create(new[] { ValidationText.Empty, ValidationText.Empty }); + var vt = ValidationText.Create(new[] { ValidationText.Empty, ValidationText.Empty }); Assert.NotSame(ValidationText.Empty, vt); Assert.Equal(2, vt.Count); // Calling Count() checks the enumeration returns no results, unlike the Count property. -#pragma warning disable CA1829 // Use Length/Count property instead of Count() when available - Assert.Equal(2, vt.Count()); -#pragma warning restore CA1829 // Use Length/Count property instead of Count() when available + Assert.Equal(2, vt.Count); Assert.Equal(string.Empty, vt[0]); Assert.Equal(string.Empty, vt[1]); diff --git a/src/ReactiveUI.Validation.sln b/src/ReactiveUI.Validation.sln index 63ff2d68..30192845 100644 --- a/src/ReactiveUI.Validation.sln +++ b/src/ReactiveUI.Validation.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30309.148 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.Validation", "ReactiveUI.Validation\ReactiveUI.Validation.csproj", "{B62AABD0-22A4-470D-B6EB-F6B3EAE668DE}" EndProject @@ -19,10 +19,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ..\version.json = ..\version.json EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.Validation.AndroidSupport", "ReactiveUI.Validation.AndroidSupport\ReactiveUI.Validation.AndroidSupport.csproj", "{B07A5CB1-3C9A-461D-B1A0-2AA642A50AF7}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.Validation.AndroidX", "ReactiveUI.Validation.AndroidX\ReactiveUI.Validation.AndroidX.csproj", "{27B18F38-BB9F-481F-A4F9-950F3B2CDA3D}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -37,14 +33,6 @@ Global {892D51D9-7A3F-4E66-A871-082A63D9BE05}.Debug|Any CPU.Build.0 = Debug|Any CPU {892D51D9-7A3F-4E66-A871-082A63D9BE05}.Release|Any CPU.ActiveCfg = Release|Any CPU {892D51D9-7A3F-4E66-A871-082A63D9BE05}.Release|Any CPU.Build.0 = Release|Any CPU - {B07A5CB1-3C9A-461D-B1A0-2AA642A50AF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B07A5CB1-3C9A-461D-B1A0-2AA642A50AF7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B07A5CB1-3C9A-461D-B1A0-2AA642A50AF7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B07A5CB1-3C9A-461D-B1A0-2AA642A50AF7}.Release|Any CPU.Build.0 = Release|Any CPU - {27B18F38-BB9F-481F-A4F9-950F3B2CDA3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {27B18F38-BB9F-481F-A4F9-950F3B2CDA3D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {27B18F38-BB9F-481F-A4F9-950F3B2CDA3D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {27B18F38-BB9F-481F-A4F9-950F3B2CDA3D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/ReactiveUI.Validation/Abstractions/IValidatableViewModel.cs b/src/ReactiveUI.Validation/Abstractions/IValidatableViewModel.cs index 217e1582..c99da670 100755 --- a/src/ReactiveUI.Validation/Abstractions/IValidatableViewModel.cs +++ b/src/ReactiveUI.Validation/Abstractions/IValidatableViewModel.cs @@ -15,5 +15,5 @@ public interface IValidatableViewModel /// /// Gets get the validation context. /// - ValidationContext ValidationContext { get; } -} \ No newline at end of file + IValidationContext ValidationContext { get; } +} diff --git a/src/ReactiveUI.Validation/Collections/ReadOnlyCollectionPooled.cs b/src/ReactiveUI.Validation/Collections/ReadOnlyCollectionPooled.cs index c120d567..223a62db 100644 --- a/src/ReactiveUI.Validation/Collections/ReadOnlyCollectionPooled.cs +++ b/src/ReactiveUI.Validation/Collections/ReadOnlyCollectionPooled.cs @@ -18,17 +18,17 @@ internal sealed class ReadOnlyCollectionPooled : IReadOnlyCollection, IDis public ReadOnlyCollectionPooled(IEnumerable items) { - T[] array = ArrayPool.Shared.Rent(16); - int index = 0; + var array = ArrayPool.Shared.Rent(16); + var index = 0; - foreach (T item in items) + foreach (var item in items) { if (array.Length == index) { - ArrayPool.Shared.Resize(ref array!, array.Length * 2, true); + ArrayPool.Shared.Resize(ref array, array.Length * 2, true); } - array[index] = item; + array![index] = item; index++; } @@ -46,7 +46,7 @@ public ReadOnlyCollectionPooled(IEnumerable items) public Enumerator GetEnumerator() => new(this); - public struct Enumerator : IEnumerator, IEnumerator + public struct Enumerator : IEnumerator { private readonly ReadOnlyCollectionPooled _readOnlyCollectionPooled; private int _index; @@ -80,7 +80,7 @@ public void Dispose() public bool MoveNext() { - ReadOnlyCollectionPooled? readOnlyCollectionPooled = _readOnlyCollectionPooled; + var readOnlyCollectionPooled = _readOnlyCollectionPooled; if ((uint)_index < (uint)readOnlyCollectionPooled.Count) { diff --git a/src/ReactiveUI.Validation/Collections/ValidationText.cs b/src/ReactiveUI.Validation/Collections/ValidationText.cs index 228c8cc2..c6e3eb67 100755 --- a/src/ReactiveUI.Validation/Collections/ValidationText.cs +++ b/src/ReactiveUI.Validation/Collections/ValidationText.cs @@ -39,7 +39,7 @@ public static IValidationText Create(IEnumerable? validationTex } // Note _texts are already validated as not-null - string[] texts = validationTexts.SelectMany(static vt => vt).ToArray(); + var texts = validationTexts.SelectMany(static vt => vt).ToArray(); return CreateValidationText(texts, texts.Length); } @@ -56,9 +56,9 @@ public static IValidationText Create(IEnumerable? validationTexts) return None; } - string[] texts = validationTexts.Where(t => t is not null).ToArray()!; + var texts = validationTexts.Where(t => t is not null).ToArray(); - return CreateValidationText(texts, texts.Length); + return CreateValidationText(texts!, texts.Length); } /// @@ -83,7 +83,7 @@ public static IValidationText Create(params string?[]? validationTexts) // Optimize code path for single item array. if (validationTexts.Length == 1) { - string? text = validationTexts[0]; + var text = validationTexts[0]; if (text is null) { @@ -93,16 +93,16 @@ public static IValidationText Create(params string?[]? validationTexts) return text.Length < 1 ? Empty : new SingleValidationText(text); } - string[] texts = ArrayPool.Shared.Rent(validationTexts.Length); + var texts = ArrayPool.Shared.Rent(validationTexts.Length); try { - int currentIndex = 0; + var currentIndex = 0; // Ensure we have no null items in the multi-item array - for (int i = 0; i < validationTexts.Length; i++) + for (var i = 0; i < validationTexts.Length; i++) { - string? text = validationTexts[i]; + var text = validationTexts[i]; if (text is null) { diff --git a/src/ReactiveUI.Validation/Components/Abstractions/IPropertyValidationComponent.cs b/src/ReactiveUI.Validation/Components/Abstractions/IPropertyValidationComponent.cs index d67d092a..5c4f2224 100644 --- a/src/ReactiveUI.Validation/Components/Abstractions/IPropertyValidationComponent.cs +++ b/src/ReactiveUI.Validation/Components/Abstractions/IPropertyValidationComponent.cs @@ -8,6 +8,4 @@ namespace ReactiveUI.Validation.Components.Abstractions; /// /// A component specifically validating one or more untyped properties. /// -public interface IPropertyValidationComponent : IValidationComponent, IValidatesProperties -{ -} \ No newline at end of file +public interface IPropertyValidationComponent : IValidationComponent, IValidatesProperties; diff --git a/src/ReactiveUI.Validation/Components/BasePropertyValidation.cs b/src/ReactiveUI.Validation/Components/BasePropertyValidation.cs index 8cf4dcfb..603a02a1 100755 --- a/src/ReactiveUI.Validation/Components/BasePropertyValidation.cs +++ b/src/ReactiveUI.Validation/Components/BasePropertyValidation.cs @@ -23,11 +23,10 @@ namespace ReactiveUI.Validation.Components; /// /// /// -/// Base class for items which are used to build a . +/// Base class for items which are used to build a . /// public abstract class BasePropertyValidation : ReactiveObject, IDisposable, IPropertyValidationComponent { - [SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Disposed by field _disposables.")] private readonly ReplaySubject _isValidSubject = new(1); private readonly HashSet _propertyNames = []; private readonly CompositeDisposable _disposables = []; @@ -115,7 +114,7 @@ protected void AddProperty(Expression> property) throw new ArgumentNullException(nameof(property)); } - string propertyName = property.Body.GetPropertyPath(); + var propertyName = property.Body.GetPropertyPath(); _propertyNames.Add(propertyName); } @@ -134,6 +133,7 @@ protected virtual void Dispose(bool disposing) if (disposing) { _disposables.Dispose(); + _isValidSubject.Dispose(); } } @@ -166,7 +166,6 @@ private void Activate() [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "Same class just generic.")] public sealed class BasePropertyValidation : BasePropertyValidation { - [SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Disposed by field _disposables.")] private readonly ReplaySubject _valueSubject = new(1); private readonly IConnectableObservable _valueConnectedObservable; private readonly Func _message; @@ -274,6 +273,7 @@ protected override void Dispose(bool disposing) if (disposing) { _disposables.Dispose(); + _valueSubject.Dispose(); } } diff --git a/src/ReactiveUI.Validation/Components/ObservableValidation.cs b/src/ReactiveUI.Validation/Components/ObservableValidation.cs deleted file mode 100644 index 9345cdd1..00000000 --- a/src/ReactiveUI.Validation/Components/ObservableValidation.cs +++ /dev/null @@ -1,385 +0,0 @@ -// Copyright (c) 2022 .NET Foundation and Contributors. All rights reserved. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Linq.Expressions; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using ReactiveUI.Validation.Collections; -using ReactiveUI.Validation.Components.Abstractions; -using ReactiveUI.Validation.Extensions; -using ReactiveUI.Validation.States; - -namespace ReactiveUI.Validation.Components; - -/// -/// -/// -/// A validation component that is based on an . Validates a single property. -/// Though in the passed observable more properties can be referenced via a call to WhenAnyValue. -/// -[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "Same class just different generic parameters.")] -public sealed class ObservableValidation : ObservableValidationBase -{ - /// - /// Initializes a new instance of the class. - /// - /// ViewModel instance. - /// ViewModel property. - /// Observable that updates the view model property validity. - /// Func to define if the viewModelProperty is valid or not. - /// Validation error message as a constant. - public ObservableValidation( - TViewModel viewModel, - Expression> viewModelProperty, - IObservable observable, - Func isValidFunc, - string message) - : this(viewModel, viewModelProperty, observable, (_, state) => isValidFunc(state), (_, _) => message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// ViewModel instance. - /// ViewModel property. - /// Observable that updates the view model property validity. - /// Func to define if the viewModelProperty is valid or not. - /// Validation error message as a constant. - public ObservableValidation( - TViewModel viewModel, - Expression> viewModelProperty, - IObservable observable, - Func isValidFunc, - string message) - : this(viewModel, viewModelProperty, observable, isValidFunc, (_, _) => message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// ViewModel instance. - /// ViewModel property. - /// Observable that updates the view model property validity. - /// Func to define if the viewModelProperty is valid or not. - /// Func to define the validation error message. - public ObservableValidation( - TViewModel viewModel, - Expression> viewModelProperty, - IObservable observable, - Func isValidFunc, - Func messageFunc) - : this(viewModel, viewModelProperty, observable, (_, state) => isValidFunc(state), (_, state) => - messageFunc(state)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// ViewModel instance. - /// ViewModel property. - /// Observable that updates the view model property validity. - /// Func to define if the viewModelProperty is valid or not. - /// Func to define the validation error message. - public ObservableValidation( - TViewModel viewModel, - Expression> viewModelProperty, - IObservable observable, - Func isValidFunc, - Func messageFunc) - : this(viewModel, viewModelProperty, observable, isValidFunc, (vm, value, isValid) => - isValid ? string.Empty : messageFunc(vm, value)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// ViewModel instance. - /// ViewModel property. - /// Observable that updates the view model property validity. - /// Func to define if the viewModelProperty is valid or not. - /// Func to define the validation error message. - public ObservableValidation( - TViewModel viewModel, - Expression> viewModelProperty, - IObservable observable, - Func isValidFunc, - Func messageFunc) - : base(viewModel, observable, isValidFunc, (vm, value, isValid) => - ValidationText.Create(messageFunc(vm, value, isValid))) => - AddProperty(viewModelProperty); - - /// - /// Initializes a new instance of the class. - /// - /// ViewModel property. - /// Observable that updates the view model property validity. - public ObservableValidation( - Expression> viewModelProperty, - IObservable observable) - : base(observable) => - AddProperty(viewModelProperty); -} - -/// -/// -/// -/// A validation component that is based on an . -/// -[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "Same class just different generic parameters.")] -public sealed class ObservableValidation : ObservableValidationBase -{ - /// - /// Initializes a new instance of the class. - /// - /// ViewModel instance. - /// Observable that updates the view model property validity. - /// Func to define if the viewModelProperty is valid or not. - /// Validation error message as a constant. - public ObservableValidation( - TViewModel viewModel, - IObservable observable, - Func isValidFunc, - string message) - : this(viewModel, observable, (_, state) => isValidFunc(state), (_, _) => message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// ViewModel instance. - /// Observable that updates the view model property validity. - /// Func to define if the viewModelProperty is valid or not. - /// Validation error message as a constant. - public ObservableValidation( - TViewModel viewModel, - IObservable observable, - Func isValidFunc, - string message) - : this(viewModel, observable, isValidFunc, (_, _) => message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// ViewModel instance. - /// Observable that updates the view model property validity. - /// Func to define if the viewModelProperty is valid or not. - /// Func to define the validation error message. - public ObservableValidation( - TViewModel viewModel, - IObservable observable, - Func isValidFunc, - Func messageFunc) - : this(viewModel, observable, (_, state) => isValidFunc(state), (_, state) => messageFunc(state)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// ViewModel instance. - /// Observable that updates the view model property validity. - /// Func to define if the viewModelProperty is valid or not. - /// Func to define the validation error message. - public ObservableValidation( - TViewModel viewModel, - IObservable observable, - Func isValidFunc, - Func messageFunc) - : this(viewModel, observable, isValidFunc, (vm, value, isValid) => - isValid ? string.Empty : messageFunc(vm, value)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// ViewModel instance. - /// Observable that updates the view model property validity. - /// Func to define if the viewModelProperty is valid or not. - /// Func to define the validation error message. - public ObservableValidation( - TViewModel viewModel, - IObservable observable, - Func isValidFunc, - Func messageFunc) - : base(viewModel, observable, isValidFunc, (vm, value, isValid) => - ValidationText.Create(messageFunc(vm, value, isValid))) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// Observable that updates the view model property validity. - public ObservableValidation(IObservable observable) - : base(observable) - { - } -} - -/// -/// -/// -/// A validation component that is based on an . -/// -[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "Same class just different generic parameters.")] -public abstract class ObservableValidationBase : ReactiveObject, IDisposable, IPropertyValidationComponent -{ - [SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Disposed by field _disposables.")] - private readonly ReplaySubject _isValidSubject = new(1); - private readonly HashSet _propertyNames = []; - private readonly CompositeDisposable _disposables = []; - private readonly IConnectableObservable _validityConnectedObservable; - private bool _isActive; - private bool _isValid; - private IValidationText? _text; - - /// - /// Initializes a new instance of the class. - /// - /// ViewModel instance. - /// Observable that updates the view model property validity. - /// Func to define if the viewModelProperty is valid or not. - /// Func to define the validation error message. - protected ObservableValidationBase( - TViewModel viewModel, - IObservable observable, - Func isValidFunc, - Func messageFunc) - : this(observable.Select(value => - { - bool isValid = isValidFunc(viewModel, value); - IValidationText message = messageFunc(viewModel, value, isValid); - return new ValidationState(isValid, message); - })) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// Observable that updates the view model property validity. - protected ObservableValidationBase(IObservable observable) - { - _isValidSubject - .Do(state => - { - _isValid = state.IsValid; - _text = state.Text; - }) - .Subscribe() - .DisposeWith(_disposables); - - _validityConnectedObservable = Observable - .Defer(() => observable) - .Multicast(_isValidSubject); - } - - /// - public int PropertyCount => _propertyNames.Count; - - /// - public IEnumerable Properties => _propertyNames.AsEnumerable(); - - /// - public IValidationText? Text - { - get - { - Activate(); - return _text; - } - } - - /// - public bool IsValid - { - get - { - Activate(); - return _isValid; - } - } - - /// - public IObservable ValidationStatusChange - { - get - { - Activate(); - return _validityConnectedObservable; - } - } - - /// - public void Dispose() - { - // Dispose of unmanaged resources. - Dispose(true); - - // Suppress finalization. - GC.SuppressFinalize(this); - } - - /// - public bool ContainsPropertyName(string propertyName, bool exclusively = false) => - exclusively - ? _propertyNames.Contains(propertyName) && - _propertyNames.Count == 1 - : _propertyNames.Contains(propertyName); - - /// - /// Disposes of the managed resources. - /// - /// - /// If its getting called by the method. - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - _disposables.Dispose(); - } - } - - /// - /// Adds a property to the list of this which this validation is associated with. - /// - /// Any type. - /// ViewModel property. - protected void AddProperty(Expression> property) - { - if (property is null) - { - throw new ArgumentNullException(nameof(property)); - } - - string propertyName = property.Body.GetPropertyPath(); - _propertyNames.Add(propertyName); - } - - private void Activate() - { - if (_isActive) - { - return; - } - - _isActive = true; - _disposables.Add(_validityConnectedObservable.Connect()); - } -} diff --git a/src/ReactiveUI.Validation/Components/ObservableValidationBase{TViewModel,TValue}.cs b/src/ReactiveUI.Validation/Components/ObservableValidationBase{TViewModel,TValue}.cs new file mode 100644 index 00000000..22312765 --- /dev/null +++ b/src/ReactiveUI.Validation/Components/ObservableValidationBase{TViewModel,TValue}.cs @@ -0,0 +1,171 @@ +// Copyright (c) 2022 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Linq.Expressions; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using ReactiveUI.Validation.Collections; +using ReactiveUI.Validation.Components.Abstractions; +using ReactiveUI.Validation.Extensions; +using ReactiveUI.Validation.States; + +namespace ReactiveUI.Validation.Components; + +/// +/// +/// +/// A validation component that is based on an . +/// +public abstract class ObservableValidationBase : ReactiveObject, IDisposable, IPropertyValidationComponent +{ + private readonly ReplaySubject _isValidSubject = new(1); + private readonly HashSet _propertyNames = []; + private readonly CompositeDisposable _disposables = []; + private readonly IConnectableObservable _validityConnectedObservable; + private bool _isActive; + private bool _isValid; + private IValidationText? _text; + + /// + /// Initializes a new instance of the class. + /// + /// ViewModel instance. + /// Observable that updates the view model property validity. + /// Func to define if the viewModelProperty is valid or not. + /// Func to define the validation error message. + protected ObservableValidationBase( + TViewModel viewModel, + IObservable observable, + Func isValidFunc, + Func messageFunc) + : this(observable.Select(value => + { + var isValid = isValidFunc(viewModel, value); + var message = messageFunc(viewModel, value, isValid); + return new ValidationState(isValid, message); + })) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Observable that updates the view model property validity. + protected ObservableValidationBase(IObservable observable) + { + _isValidSubject + .Do(state => + { + _isValid = state.IsValid; + _text = state.Text; + }) + .Subscribe() + .DisposeWith(_disposables); + + _validityConnectedObservable = Observable + .Defer(() => observable) + .Multicast(_isValidSubject); + } + + /// + public int PropertyCount => _propertyNames.Count; + + /// + public IEnumerable Properties => _propertyNames.AsEnumerable(); + + /// + public IValidationText? Text + { + get + { + Activate(); + return _text; + } + } + + /// + public bool IsValid + { + get + { + Activate(); + return _isValid; + } + } + + /// + public IObservable ValidationStatusChange + { + get + { + Activate(); + return _validityConnectedObservable; + } + } + + /// + public void Dispose() + { + // Dispose of unmanaged resources. + Dispose(true); + + // Suppress finalization. + GC.SuppressFinalize(this); + } + + /// + public bool ContainsPropertyName(string propertyName, bool exclusively = false) => + exclusively + ? _propertyNames.Contains(propertyName) && + _propertyNames.Count == 1 + : _propertyNames.Contains(propertyName); + + /// + /// Disposes of the managed resources. + /// + /// + /// If its getting called by the method. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _disposables.Dispose(); + _isValidSubject.Dispose(); + } + } + + /// + /// Adds a property to the list of this which this validation is associated with. + /// + /// Any type. + /// ViewModel property. + protected void AddProperty(Expression> property) + { + if (property is null) + { + throw new ArgumentNullException(nameof(property)); + } + + var propertyName = property.Body.GetPropertyPath(); + _propertyNames.Add(propertyName); + } + + private void Activate() + { + if (_isActive) + { + return; + } + + _isActive = true; + _disposables.Add(_validityConnectedObservable.Connect()); + } +} diff --git a/src/ReactiveUI.Validation/Components/ObservableValidation{TViewModel,TValue,TProp}.cs b/src/ReactiveUI.Validation/Components/ObservableValidation{TViewModel,TValue,TProp}.cs new file mode 100644 index 00000000..d561a381 --- /dev/null +++ b/src/ReactiveUI.Validation/Components/ObservableValidation{TViewModel,TValue,TProp}.cs @@ -0,0 +1,123 @@ +// Copyright (c) 2022 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.Linq.Expressions; +using ReactiveUI.Validation.Collections; +using ReactiveUI.Validation.States; + +namespace ReactiveUI.Validation.Components; + +/// +/// +/// +/// A validation component that is based on an . Validates a single property. +/// Though in the passed observable more properties can be referenced via a call to WhenAnyValue. +/// +public sealed class ObservableValidation : ObservableValidationBase +{ + /// + /// Initializes a new instance of the class. + /// + /// ViewModel instance. + /// ViewModel property. + /// Observable that updates the view model property validity. + /// Func to define if the viewModelProperty is valid or not. + /// Validation error message as a constant. + public ObservableValidation( + TViewModel viewModel, + Expression> viewModelProperty, + IObservable observable, + Func isValidFunc, + string message) + : this(viewModel, viewModelProperty, observable, (_, state) => isValidFunc(state), (_, _) => message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// ViewModel instance. + /// ViewModel property. + /// Observable that updates the view model property validity. + /// Func to define if the viewModelProperty is valid or not. + /// Validation error message as a constant. + public ObservableValidation( + TViewModel viewModel, + Expression> viewModelProperty, + IObservable observable, + Func isValidFunc, + string message) + : this(viewModel, viewModelProperty, observable, isValidFunc, (_, _) => message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// ViewModel instance. + /// ViewModel property. + /// Observable that updates the view model property validity. + /// Func to define if the viewModelProperty is valid or not. + /// Func to define the validation error message. + public ObservableValidation( + TViewModel viewModel, + Expression> viewModelProperty, + IObservable observable, + Func isValidFunc, + Func messageFunc) + : this(viewModel, viewModelProperty, observable, (_, state) => isValidFunc(state), (_, state) => + messageFunc(state)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// ViewModel instance. + /// ViewModel property. + /// Observable that updates the view model property validity. + /// Func to define if the viewModelProperty is valid or not. + /// Func to define the validation error message. + public ObservableValidation( + TViewModel viewModel, + Expression> viewModelProperty, + IObservable observable, + Func isValidFunc, + Func messageFunc) + : this(viewModel, viewModelProperty, observable, isValidFunc, (vm, value, isValid) => + isValid ? string.Empty : messageFunc(vm, value)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// ViewModel instance. + /// ViewModel property. + /// Observable that updates the view model property validity. + /// Func to define if the viewModelProperty is valid or not. + /// Func to define the validation error message. + public ObservableValidation( + TViewModel viewModel, + Expression> viewModelProperty, + IObservable observable, + Func isValidFunc, + Func messageFunc) + : base(viewModel, observable, isValidFunc, (vm, value, isValid) => + ValidationText.Create(messageFunc(vm, value, isValid))) => + AddProperty(viewModelProperty); + + /// + /// Initializes a new instance of the class. + /// + /// ViewModel property. + /// Observable that updates the view model property validity. + public ObservableValidation( + Expression> viewModelProperty, + IObservable observable) + : base(observable) => + AddProperty(viewModelProperty); +} diff --git a/src/ReactiveUI.Validation/Components/ObservableValidation{TViewModel,TValue}.cs b/src/ReactiveUI.Validation/Components/ObservableValidation{TViewModel,TValue}.cs new file mode 100644 index 00000000..953dd67c --- /dev/null +++ b/src/ReactiveUI.Validation/Components/ObservableValidation{TViewModel,TValue}.cs @@ -0,0 +1,118 @@ +// Copyright (c) 2022 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Linq.Expressions; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using ReactiveUI.Validation.Collections; +using ReactiveUI.Validation.Components.Abstractions; +using ReactiveUI.Validation.Extensions; +using ReactiveUI.Validation.States; + +namespace ReactiveUI.Validation.Components; + +/// +/// +/// +/// A validation component that is based on an . +/// +public sealed class ObservableValidation : ObservableValidationBase +{ + /// + /// Initializes a new instance of the class. + /// + /// ViewModel instance. + /// Observable that updates the view model property validity. + /// Func to define if the viewModelProperty is valid or not. + /// Validation error message as a constant. + public ObservableValidation( + TViewModel viewModel, + IObservable observable, + Func isValidFunc, + string message) + : this(viewModel, observable, (_, state) => isValidFunc(state), (_, _) => message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// ViewModel instance. + /// Observable that updates the view model property validity. + /// Func to define if the viewModelProperty is valid or not. + /// Validation error message as a constant. + public ObservableValidation( + TViewModel viewModel, + IObservable observable, + Func isValidFunc, + string message) + : this(viewModel, observable, isValidFunc, (_, _) => message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// ViewModel instance. + /// Observable that updates the view model property validity. + /// Func to define if the viewModelProperty is valid or not. + /// Func to define the validation error message. + public ObservableValidation( + TViewModel viewModel, + IObservable observable, + Func isValidFunc, + Func messageFunc) + : this(viewModel, observable, (_, state) => isValidFunc(state), (_, state) => messageFunc(state)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// ViewModel instance. + /// Observable that updates the view model property validity. + /// Func to define if the viewModelProperty is valid or not. + /// Func to define the validation error message. + public ObservableValidation( + TViewModel viewModel, + IObservable observable, + Func isValidFunc, + Func messageFunc) + : this(viewModel, observable, isValidFunc, (vm, value, isValid) => + isValid ? string.Empty : messageFunc(vm, value)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// ViewModel instance. + /// Observable that updates the view model property validity. + /// Func to define if the viewModelProperty is valid or not. + /// Func to define the validation error message. + public ObservableValidation( + TViewModel viewModel, + IObservable observable, + Func isValidFunc, + Func messageFunc) + : base(viewModel, observable, isValidFunc, (vm, value, isValid) => + ValidationText.Create(messageFunc(vm, value, isValid))) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Observable that updates the view model property validity. + public ObservableValidation(IObservable observable) + : base(observable) + { + } +} diff --git a/src/ReactiveUI.Validation/Contexts/IValidationContext.cs b/src/ReactiveUI.Validation/Contexts/IValidationContext.cs new file mode 100644 index 00000000..cc219be2 --- /dev/null +++ b/src/ReactiveUI.Validation/Contexts/IValidationContext.cs @@ -0,0 +1,59 @@ +// Copyright (c) 2022 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Reactive.Disposables; +using DynamicData; +using ReactiveUI.Validation.Components.Abstractions; + +namespace ReactiveUI.Validation.Contexts; + +/// +/// +/// +/// +/// The overall context for a view model under which validation takes place. +/// +/// +/// Contains all of the instances +/// applicable to the view model. +/// +public interface IValidationContext : IValidationComponent, IReactiveObject, ICancelable +{ + /// + /// Gets an observable for the Valid state. + /// + IObservable Valid { get; } + + /// + /// Gets get the list of validations. + /// + IObservableList Validations { get; } + + /// + /// Adds a validation into the validations collection. + /// + /// Validation component to be added into the collection. + void Add(IValidationComponent validation); + + /// + /// Removes a validation from the validations collection. + /// + /// Validation component to be removed from the collection. + void Remove(IValidationComponent validation); + + /// + /// Removes many validation components from the validations collection. + /// + /// Validation components to be removed from the collection. + void RemoveMany(IEnumerable validations); + + /// + /// Returns if the whole context is valid checking all the validations. + /// + /// Returns true if the is valid, otherwise false. + bool GetIsValid(); +} diff --git a/src/ReactiveUI.Validation/Contexts/ValidationContext.cs b/src/ReactiveUI.Validation/Contexts/ValidationContext.cs index 90d2974f..d8436a37 100755 --- a/src/ReactiveUI.Validation/Contexts/ValidationContext.cs +++ b/src/ReactiveUI.Validation/Contexts/ValidationContext.cs @@ -6,7 +6,6 @@ using System; using System.Buffers; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reactive.Concurrency; @@ -27,22 +26,20 @@ namespace ReactiveUI.Validation.Contexts; /// The overall context for a view model under which validation takes place. /// /// -/// Contains all of the instances +/// Contains all of the instances /// applicable to the view model. /// -[SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Field _disposables disposes the items.")] -public class ValidationContext : ReactiveObject, IDisposable, IValidationComponent +public class ValidationContext : ReactiveObject, IValidationContext { - private readonly SourceCache _validationSource = new(static x => x); private readonly ReplaySubject _validationStatusChange = new(1); private readonly ReplaySubject _validSubject = new(1); - private readonly ReadOnlyObservableCollection _validations; private readonly IConnectableObservable _validationConnectable; private readonly ObservableAsPropertyHelper _validationText; private readonly ObservableAsPropertyHelper _isValid; private readonly CompositeDisposable _disposables = []; + private SourceList _validationSource = new(); private bool _isActive; /// @@ -53,18 +50,14 @@ public ValidationContext(IScheduler? scheduler = null) { scheduler ??= CurrentThreadScheduler.Instance; var changeSets = _validationSource.Connect().ObserveOn(scheduler); - - changeSets - .Bind(out _validations) - .Subscribe() - .DisposeWith(_disposables); + Validations = changeSets.AsObservableList(); _validationConnectable = changeSets .StartWithEmpty() .AutoRefreshOnObservable(x => x.ValidationStatusChange) .QueryWhenChanged(static x => { - using ReadOnlyCollectionPooled validationComponents = new(x.Items); + using ReadOnlyCollectionPooled validationComponents = new(x); return validationComponents.Count is 0 || validationComponents.All(v => v.IsValid); }) .Multicast(_validSubject); @@ -102,10 +95,9 @@ public IObservable Valid /// /// Gets get the list of validations. /// - public ReadOnlyObservableCollection Validations => _validations; + public IObservableList Validations { get; private set; } /// - [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "Reviewed.")] public bool IsValid { get @@ -135,29 +127,37 @@ public IValidationText Text } } + /// + /// Gets a value indicating whether this instance is disposed. + /// + /// + /// true if this instance is disposed; otherwise, false. + /// + public bool IsDisposed => _disposables.IsDisposed; + /// /// Adds a validation into the validations collection. /// /// Validation component to be added into the collection. - public void Add(IValidationComponent validation) => _validationSource.AddOrUpdate(validation); + public void Add(IValidationComponent validation) => _validationSource.Add(validation); /// /// Removes a validation from the validations collection. /// /// Validation component to be removed from the collection. - public void Remove(IValidationComponent validation) => _validationSource.RemoveKey(validation); + public void Remove(IValidationComponent validation) => _validationSource.Remove(validation); /// /// Removes many validation components from the validations collection. /// /// Validation components to be removed from the collection. - public void RemoveMany(IEnumerable validations) => _validationSource.RemoveKeys(validations); + public void RemoveMany(IEnumerable validations) => _validationSource.RemoveMany(validations); /// /// Returns if the whole context is valid checking all the validations. /// /// Returns true if the is valid, otherwise false. - public bool GetIsValid() => _validations.Count == 0 || _validations.All(v => v.IsValid); + public bool GetIsValid() => Validations.Count == 0 || Validations.Items.All(v => v.IsValid); /// public void Dispose() @@ -175,9 +175,19 @@ public void Dispose() /// If its getting called by the method. protected virtual void Dispose(bool disposing) { - if (disposing) + if (!_disposables.IsDisposed && disposing) { _disposables.Dispose(); + _isValid.Dispose(); + _validationText.Dispose(); + _validationStatusChange.Dispose(); + _validSubject.Dispose(); + _validationSource.Clear(); + Validations.Dispose(); + _validationSource.Dispose(); + + Validations = null!; + _validationSource = null!; } } @@ -200,15 +210,13 @@ private void Activate() /// private IValidationText BuildText() { - IValidationText[] validationComponents = ArrayPool.Shared.Rent(_validations.Count); + var validationComponents = ArrayPool.Shared.Rent(Validations.Count); try { - int currentIndex = 0; - for (int i = 0; i < _validations.Count; i++) + var currentIndex = 0; + foreach (var validationComponent in Validations.Items) { - IValidationComponent validationComponent = _validations[i]; - if (validationComponent.IsValid || validationComponent.Text is null) { continue; diff --git a/src/ReactiveUI.Validation/Extensions/ArrayPoolExtensions.cs b/src/ReactiveUI.Validation/Extensions/ArrayPoolExtensions.cs index 9eff10b3..68117bae 100644 --- a/src/ReactiveUI.Validation/Extensions/ArrayPoolExtensions.cs +++ b/src/ReactiveUI.Validation/Extensions/ArrayPoolExtensions.cs @@ -44,8 +44,8 @@ public static void Resize(this ArrayPool pool, ref T[]? array, int newSize // the BCL: if the new size is greater than the length of the current array, copy all the // items from the original array into the new one. Otherwise, copy as many items as possible, // until the new array is completely filled, and ignore the remaining items in the first array. - T[] newArray = pool.Rent(newSize); - int itemsToCopy = Math.Min(array.Length, newSize); + var newArray = pool.Rent(newSize); + var itemsToCopy = Math.Min(array.Length, newSize); Array.Copy(array, 0, newArray, 0, itemsToCopy); diff --git a/src/ReactiveUI.Validation/Extensions/ValidatableViewModelExtensions.cs b/src/ReactiveUI.Validation/Extensions/ValidatableViewModelExtensions.cs index e9506cfc..bdd72bd2 100644 --- a/src/ReactiveUI.Validation/Extensions/ValidatableViewModelExtensions.cs +++ b/src/ReactiveUI.Validation/Extensions/ValidatableViewModelExtensions.cs @@ -471,7 +471,7 @@ public static void ClearValidationRules( var validationComponents = viewModel .ValidationContext - .Validations + .Validations.Items .OfType() .Where(validation => validation.ContainsProperty(viewModelProperty)) .ToList(); @@ -495,7 +495,7 @@ public static void ClearValidationRules(this TViewModel viewModel) throw new ArgumentNullException(nameof(viewModel)); } - viewModel.ValidationContext.RemoveMany(viewModel.ValidationContext.Validations); + viewModel.ValidationContext.RemoveMany(viewModel.ValidationContext.Validations.Items); } /// @@ -537,4 +537,4 @@ private static ValidationHelper RegisterValidation( validation.Dispose(); })); } -} \ No newline at end of file +} diff --git a/src/ReactiveUI.Validation/Extensions/ValidationContextExtensions.cs b/src/ReactiveUI.Validation/Extensions/ValidationContextExtensions.cs index 9c80e7eb..05a73618 100755 --- a/src/ReactiveUI.Validation/Extensions/ValidationContextExtensions.cs +++ b/src/ReactiveUI.Validation/Extensions/ValidationContextExtensions.cs @@ -9,7 +9,6 @@ using System.Linq.Expressions; using System.Reactive.Linq; using DynamicData; -using DynamicData.Binding; using ReactiveUI.Validation.Components; using ReactiveUI.Validation.Components.Abstractions; using ReactiveUI.Validation.Contexts; @@ -22,7 +21,7 @@ namespace ReactiveUI.Validation.Extensions; /// public static class ValidationContextExtensions { - private static IValidationState[] InitialValidationStates { get; } = { ValidationState.Valid }; + private static IValidationState[] InitialValidationStates { get; } = [ValidationState.Valid]; /// /// Resolves the for a specified property in a reactive fashion. @@ -34,7 +33,7 @@ public static class ValidationContextExtensions /// Indicates if the ViewModel property to find is unique. /// Returns a collection of objects. public static IObservable> ObserveFor( - this ValidationContext context, + this IValidationContext context, Expression> viewModelProperty, bool strict = true) { @@ -50,7 +49,7 @@ public static IObservable> ObserveFor validations .OfType() diff --git a/src/ReactiveUI.Validation/Helpers/ReactiveValidationObject.cs b/src/ReactiveUI.Validation/Helpers/ReactiveValidationObject.cs index 75db2891..2cf37806 100644 --- a/src/ReactiveUI.Validation/Helpers/ReactiveValidationObject.cs +++ b/src/ReactiveUI.Validation/Helpers/ReactiveValidationObject.cs @@ -9,9 +9,9 @@ using System.ComponentModel; using System.Linq; using System.Reactive.Concurrency; +using System.Reactive.Disposables; using System.Reactive.Linq; using DynamicData; -using DynamicData.Binding; using ReactiveUI.Validation.Abstractions; using ReactiveUI.Validation.Collections; using ReactiveUI.Validation.Components.Abstractions; @@ -25,10 +25,11 @@ namespace ReactiveUI.Validation.Helpers; /// /// Base class for ReactiveObjects that support validation. /// -public abstract class ReactiveValidationObject : ReactiveObject, IValidatableViewModel, INotifyDataErrorInfo +public abstract class ReactiveValidationObject : ReactiveObject, IValidatableViewModel, INotifyDataErrorInfo, IDisposable { - private readonly HashSet _mentionedPropertyNames = []; - private readonly IValidationTextFormatter _formatter; + private CompositeDisposable _disposables = []; + private IValidationTextFormatter _formatter; + private HashSet _mentionedPropertyNames = []; private bool _hasErrors; /// @@ -51,8 +52,9 @@ protected ReactiveValidationObject( SingleLineFormatter.Default; ValidationContext = new ValidationContext(scheduler); + ValidationContext.DisposeWith(_disposables); ValidationContext.Validations - .ToObservableChangeSet() + .Connect() .ToCollection() .Select(components => components .Select(component => component @@ -61,7 +63,7 @@ protected ReactiveValidationObject( .Merge() .StartWith(ValidationContext)) .Switch() - .Subscribe(OnValidationStatusChange); + .Subscribe(OnValidationStatusChange).DisposeWith(_disposables); } /// @@ -75,7 +77,7 @@ public bool HasErrors } /// - public ValidationContext ValidationContext { get; } + public IValidationContext ValidationContext { get; private set; } /// /// Returns a collection of error messages, required by the INotifyDataErrorInfo interface. @@ -93,6 +95,16 @@ public virtual IEnumerable GetErrors(string? propertyName) => .Select(state => _formatter.Format(state.Text ?? ValidationText.None)) .ToArray(); + /// + /// Releases unmanaged and - optionally - managed resources. + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + /// /// Raises the event. /// @@ -100,12 +112,29 @@ public virtual IEnumerable GetErrors(string? propertyName) => protected void RaiseErrorsChanged(string propertyName = "") => ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); + /// + /// Disposes the specified disposing. + /// + /// if set to true [disposing]. + protected virtual void Dispose(bool disposing) + { + if (!_disposables.IsDisposed && disposing) + { + _disposables.Dispose(); + _mentionedPropertyNames.Clear(); + _formatter = null!; + _mentionedPropertyNames = null!; + _disposables = null!; + ValidationContext = null!; + } + } + /// /// Selects validation components that are invalid. /// /// Returns the invalid property validations. private IEnumerable SelectInvalidPropertyValidations() => - ValidationContext.Validations + ValidationContext.Validations.Items .OfType() .Where(validation => !validation.IsValid); diff --git a/src/ReactiveUI.Validation/Helpers/ValidationHelper.cs b/src/ReactiveUI.Validation/Helpers/ValidationHelper.cs index 2dc43595..8dcba027 100755 --- a/src/ReactiveUI.Validation/Helpers/ValidationHelper.cs +++ b/src/ReactiveUI.Validation/Helpers/ValidationHelper.cs @@ -21,7 +21,7 @@ public class ValidationHelper : ReactiveObject, IDisposable private readonly ObservableAsPropertyHelper _message; private readonly ObservableAsPropertyHelper _isValid; private readonly IValidationComponent _validation; - private readonly IDisposable? _cleanup; + private IDisposable? _cleanup; /// /// Initializes a new instance of the class. @@ -50,7 +50,7 @@ public ValidationHelper(IValidationComponent validation, IDisposable? cleanup = /// /// Gets the current (optional) validation message. /// - public IValidationText? Message => _message.Value; + public IValidationText Message => _message.Value; /// /// Gets the observable for validation state changes. @@ -81,5 +81,6 @@ protected virtual void Dispose(bool disposing) _isValid.Dispose(); _message.Dispose(); _cleanup?.Dispose(); + _cleanup = null; } } diff --git a/src/ReactiveUI.Validation/ReactiveUI.Validation.csproj b/src/ReactiveUI.Validation/ReactiveUI.Validation.csproj index 73cc5564..fbaa02cc 100644 --- a/src/ReactiveUI.Validation/ReactiveUI.Validation.csproj +++ b/src/ReactiveUI.Validation/ReactiveUI.Validation.csproj @@ -1,18 +1,13 @@  - MonoAndroid13.0;Xamarin.iOS10;Xamarin.Mac20;Xamarin.TVOS10;tizen40;netstandard2.0;net6.0;net7.0;net7.0-android;net7.0-ios;net7.0-tvos;net7.0-macos;net7.0-maccatalyst;net8.0;net8.0-android;net8.0-ios;net8.0-tvos;net8.0-macos;net8.0-maccatalyst + netstandard2.0;net6.0;net7.0;net7.0-android;net7.0-ios;net7.0-tvos;net7.0-macos;net7.0-maccatalyst;net8.0;net8.0-android;net8.0-ios;net8.0-tvos;net8.0-macos;net8.0-maccatalyst $(TargetFrameworks);net462;net472;net6.0-windows10.0.17763.0;net7.0-windows10.0.17763.0;net8.0-windows10.0.17763.0 $(NoWarn);CS1591 enable - - - - - - - + + diff --git a/src/ReactiveUI.Validation/States/IValidationState.cs b/src/ReactiveUI.Validation/States/IValidationState.cs index 9502a273..1032d0b4 100644 --- a/src/ReactiveUI.Validation/States/IValidationState.cs +++ b/src/ReactiveUI.Validation/States/IValidationState.cs @@ -3,6 +3,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System; using ReactiveUI.Validation.Collections; namespace ReactiveUI.Validation.States; diff --git a/src/ReactiveUI.Validation/ValidationBindings/ValidationBinding.cs b/src/ReactiveUI.Validation/ValidationBindings/ValidationBinding.cs index 439ed783..c9c84e7e 100755 --- a/src/ReactiveUI.Validation/ValidationBindings/ValidationBinding.cs +++ b/src/ReactiveUI.Validation/ValidationBindings/ValidationBinding.cs @@ -23,7 +23,7 @@ namespace ReactiveUI.Validation.ValidationBindings; /// public sealed class ValidationBinding : IValidationBinding { - private readonly IDisposable _disposable; + private IDisposable _disposable; private ValidationBinding(IObservable bindingObservable) => _disposable = bindingObservable.Subscribe(); @@ -326,7 +326,7 @@ public static IValidationBinding ForViewModel( if (viewProperty is null) { - throw new ArgumentNullException(nameof(view)); + throw new ArgumentNullException(nameof(viewProperty)); } formatter ??= Locator.Current.GetService>() ?? @@ -383,7 +383,7 @@ private static IObservable BindToView( .Do( x => setter(x.host, x.val, viewExpression.GetArgumentsArray()), ex => LogHost.Default.Error(ex, $"{viewExpression} Binding received an Exception!")) - .Select(v => Unit.Default); + .Select(_ => Unit.Default); } /// @@ -395,6 +395,7 @@ private void Dispose(bool disposing) if (disposing) { _disposable.Dispose(); + _disposable = null!; } } -} \ No newline at end of file +} diff --git a/version.json b/version.json index 6b10bab0..67c0bb52 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "version": "3.1", + "version": "4.0", "publicReleaseRefSpec": [ "^refs/heads/main$", "^refs/heads/latest$",