Skip to content

Commit 737b280

Browse files
authored
breaking: seal ValidationState as record, performance improvements, replace Polyfill NuGet with local polyfills (#878)
* breaking: seal ValidationState as record, performance improvements, replace Polyfill NuGet with local polyfills - Convert ValidationState from a class with primary constructor to a sealed record, adding IEquatable<ValidationState> support and init accessors for IsValid. - Remove Polyfill NuGet package from the core library and replace with local polyfill files for RequiresDynamicCodeAttribute, RequiresUnreferencedCodeAttribute, IsExternalInit, NotNullAttribute, and CallerArgumentExpressionAttribute. - Remove all 28 #if NET6_0_OR_GREATER / #endif guards around AOT attributes, making them unconditionally applied via the local polyfills. - Optimize ValidationStateComparer to compare validation text element-by-element instead of joining to a single string, and use ReferenceEquals for fast-path null/identity checks. - Optimize HashCode computation in ValidationStateComparer using a local HashCode polyfill with per-element ordinal string hashing. - Add lazy activation pattern to BasePropertyValidation and ValidationContext to defer observable subscriptions until first access. - Convert internal collection types (ArrayValidationText, SingleValidationText, ReadOnlyDisposableCollection) to use primary constructors and add XML documentation. - Add #if NET8_0_OR_GREATER guard in ExpressionExtensions to use the faster char overload of string.Join on supported frameworks. - Expand ValidationText with internal helper methods (CreateValidationText, CopyArray) and optimize array-backed paths to avoid unnecessary allocations. - Add comprehensive unit tests for InternalCollectionTests, NotifyDataErrorInfoTests, ValidationContextTests, and ValidationStateComparerTests. - Fix minor XML doc typos in IValidatableViewModel and IValidationContext. - Update README to replace StackOverflow link with GitHub Discussions and remove duplicate Feedback section. - Update API approval baselines to reflect the sealed record change to ValidationState. * Update the copyright year * Update core team info * Stop producing nupkg for sample apps * Cleanup the codecov.yml file
1 parent 55fc677 commit 737b280

File tree

101 files changed

+1701
-324
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

101 files changed

+1701
-324
lines changed

README.md

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,32 @@
1313

1414
# ReactiveUI.Validation
1515

16-
Validation for ReactiveUI based solutions, functioning in a reactive way. `ReactiveUI.Validation` was originally developed by [@jcmm33](https://github.com/jcmm33) as [Vistian.Reactive.Validation](https://github.com/jcmm33/ReactiveUI.Validation), and then refactored and updated by [Àlex Martínez Morón](https://github.com/alexmartinezm) and the [ReactiveUI Core Team](https://github.com/reactiveui/ReactiveUI#core-team).
16+
Validation for ReactiveUI based solutions, functioning in a reactive way.
17+
18+
## Core Team
19+
20+
<table>
21+
<tbody>
22+
<tr>
23+
<td align="center" valign="top">
24+
<img width="100" height="100" src="https://github.com/ChrisPulman.png?s=150">
25+
<br>
26+
<a href="https://github.com/ChrisPulman">Chris Pulman</a>
27+
<p>London, UK</p>
28+
</td>
29+
<td align="center" valign="top">
30+
<img width="100" height="100" src="https://github.com/glennawatson.png?s=150">
31+
<br>
32+
<a href="https://github.com/glennawatson">Glenn Watson</a>
33+
<p>Melbourne, Australia</p>
34+
</td>
35+
</tr>
36+
</tbody>
37+
</table>
38+
39+
## Alumni & History
40+
41+
ReactiveUI.Validation was originally developed by [@jcmm33](https://github.com/jcmm33) as [Vistian.Reactive.Validation](https://github.com/jcmm33/ReactiveUI.Validation), and then refactored and updated by [Àlex Martínez Morón](https://github.com/alexmartinezm) and the ReactiveUI community.
1742

1843
## Supported Platforms
1944

@@ -282,17 +307,13 @@ In essence, ReactiveUI.Validation is a relatively simple model of the `Validatio
282307
4. Validation text can reference either the ViewModel or properties which comprise the validation rule e.g. include text entered as part of validation message.
283308
5. Validation text output can be adjusted using custom formatters, not only allowing for single & multiline output but also for platforms like Android it should be possible to achieve richer renderings i.e. Bold/italics.
284309

285-
## Feedback
286-
287-
Please use [GitHub issues](https://github.com/reactiveui/ReactiveUI.Validation/issues) for questions, comments, or bug reports.
288-
289310
## Contribute
290311

291312
ReactiveUI.Validation is developed under an OSI-approved open source license, making it freely usable and distributable, even for commercial use. We love the people who are involved in this project, and we'd love to have you on board, especially if you are just getting started or have never contributed to open-source before.
292313

293314
So here's to you, lovely person who wants to join us — this is how you can support us:
294315

295-
* [Responding to questions on StackOverflow](https://stackoverflow.com/questions/tagged/reactiveui)
316+
* [Responding to questions on GitHub Discussions](https://github.com/reactiveui/ReactiveUI.Validation/discussions)
296317
* [Passing on knowledge and teaching the next generation of developers](http://ericsink.com/entries/dont_use_rxui.html)
297318
* Submitting documentation updates where you see fit or lacking.
298319
* Making contributions to the code base.

codecov.yml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
### YamlMime:ManagedReference
21
ignore:
3-
- src/tests
4-
- "**/Tests/"
5-
- "**/*.Tests/"
6-
- "**/*.Test/"
2+
- "src/tests/**"
3+
- "src/samples/**"
4+
- "**/LoginApp*/**"
5+
- "**/AvaloniaApplication1*/**"

src/Directory.Build.props

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<RepositoryType>git</RepositoryType>
2222
<GenerateDocumentationFile>true</GenerateDocumentationFile>
2323
<IsTestProject>$(MSBuildProjectName.Contains('Tests'))</IsTestProject>
24+
<IsSampleProject>$(MSBuildProjectDirectory.Contains('samples'))</IsSampleProject>
2425
<DebugType>Embedded</DebugType>
2526
<!-- Optional: Publish the repository URL in the built .nupkg (in the NuSpec <Repository> element) -->
2627
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -36,6 +37,11 @@
3637

3738
<PropertyGroup Condition="'$(IsTestProject)' == 'true'">
3839
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
40+
<IsPackable>false</IsPackable>
41+
</PropertyGroup>
42+
43+
<PropertyGroup Condition="'$(IsSampleProject)' == 'true'">
44+
<IsPackable>false</IsPackable>
3945
</PropertyGroup>
4046

4147
<PropertyGroup>

src/Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
<ItemGroup Label="Core packages">
88
<PackageVersion Include="ReactiveUI" Version="23.1.1" />
99
<PackageVersion Include="ReactiveUI.AndroidX" Version="23.1.1" />
10-
<PackageVersion Include="System.Collections.Immutable" Version="10.0.3" />
1110
<PackageVersion Include="Polyfill" Version="9.12.0" />
11+
<PackageVersion Include="System.Collections.Immutable" Version="10.0.3" />
1212
<PackageVersion Include="Xamarin.Google.Android.Material" Version="1.13.0.1" />
1313
<PackageVersion Include="Xamarin.AndroidX.Lifecycle.LiveData.Core.Ktx" Version="2.10.0.1" />
1414
<PackageVersion Include="Xamarin.AndroidX.Lifecycle.Process" Version="2.10.0.1" />

src/ReactiveUI.Validation.AndroidX/Extensions/ViewForExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2025 ReactiveUI and Contributors. All rights reserved.
1+
// Copyright (c) 2019-2026 ReactiveUI and Contributors. All rights reserved.
22
// Licensed to the ReactiveUI and Contributors under one or more agreements.
33
// The ReactiveUI and Contributors licenses this file to you under the MIT license.
44
// See the LICENSE file in the project root for full license information.

src/ReactiveUI.Validation/Abstractions/IValidatableViewModel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2025 ReactiveUI and Contributors. All rights reserved.
1+
// Copyright (c) 2019-2026 ReactiveUI and Contributors. All rights reserved.
22
// Licensed to the ReactiveUI and Contributors under one or more agreements.
33
// The ReactiveUI and Contributors licenses this file to you under the MIT license.
44
// See the LICENSE file in the project root for full license information.
@@ -13,7 +13,7 @@ namespace ReactiveUI.Validation.Abstractions;
1313
public interface IValidatableViewModel
1414
{
1515
/// <summary>
16-
/// Gets get the validation context.
16+
/// Gets the validation context.
1717
/// </summary>
1818
IValidationContext ValidationContext { get; }
1919
}
Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2025 ReactiveUI and Contributors. All rights reserved.
1+
// Copyright (c) 2019-2026 ReactiveUI and Contributors. All rights reserved.
22
// Licensed to the ReactiveUI and Contributors under one or more agreements.
33
// The ReactiveUI and Contributors licenses this file to you under the MIT license.
44
// See the LICENSE file in the project root for full license information.
@@ -8,19 +8,37 @@
88

99
namespace ReactiveUI.Validation.Collections;
1010

11-
internal sealed class ArrayValidationText : IValidationText
11+
/// <summary>
12+
/// An <see cref="IValidationText"/> implementation that wraps an array of validation message strings.
13+
/// </summary>
14+
/// <param name="texts">The array of validation message strings.</param>
15+
internal sealed class ArrayValidationText(string[] texts) : IValidationText
1216
{
13-
private readonly string[] _texts;
14-
15-
internal ArrayValidationText(string[] texts) => _texts = texts;
16-
17-
public int Count => _texts.Length;
18-
19-
public string this[int index] => _texts[index];
20-
21-
public IEnumerator<string> GetEnumerator() => ((IEnumerable<string>)_texts).GetEnumerator();
22-
23-
IEnumerator IEnumerable.GetEnumerator() => _texts.GetEnumerator();
24-
25-
public string ToSingleLine(string? separator) => string.Join(separator, _texts);
17+
/// <summary>
18+
/// Gets the number of validation messages in the array.
19+
/// </summary>
20+
public int Count => texts.Length;
21+
22+
/// <summary>
23+
/// Gets the validation message at the specified index.
24+
/// </summary>
25+
/// <param name="index">The zero-based index of the message to retrieve.</param>
26+
/// <returns>The validation message string at the specified index.</returns>
27+
public string this[int index] => texts[index];
28+
29+
/// <summary>
30+
/// Returns an enumerator that iterates through the validation messages.
31+
/// </summary>
32+
/// <returns>An enumerator for the validation message strings.</returns>
33+
public IEnumerator<string> GetEnumerator() => ((IEnumerable<string>)texts).GetEnumerator();
34+
35+
/// <inheritdoc/>
36+
IEnumerator IEnumerable.GetEnumerator() => texts.GetEnumerator();
37+
38+
/// <summary>
39+
/// Joins all validation messages into a single string using the specified separator.
40+
/// </summary>
41+
/// <param name="separator">The string to use as a separator between messages.</param>
42+
/// <returns>A single string containing all validation messages joined by the separator.</returns>
43+
public string ToSingleLine(string? separator) => string.Join(separator, texts);
2644
}

src/ReactiveUI.Validation/Collections/IValidationText.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2025 ReactiveUI and Contributors. All rights reserved.
1+
// Copyright (c) 2019-2026 ReactiveUI and Contributors. All rights reserved.
22
// Licensed to the ReactiveUI and Contributors under one or more agreements.
33
// The ReactiveUI and Contributors licenses this file to you under the MIT license.
44
// See the LICENSE file in the project root for full license information.

src/ReactiveUI.Validation/Collections/ReadOnlyDisposableCollection{T}.cs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2025 ReactiveUI and Contributors. All rights reserved.
1+
// Copyright (c) 2019-2026 ReactiveUI and Contributors. All rights reserved.
22
// Licensed to the ReactiveUI and Contributors under one or more agreements.
33
// The ReactiveUI and Contributors licenses this file to you under the MIT license.
44
// See the LICENSE file in the project root for full license information.
@@ -10,9 +10,22 @@
1010

1111
namespace ReactiveUI.Validation.Collections;
1212

13+
/// <summary>
14+
/// A read-only collection that takes a snapshot of the source enumeration into an
15+
/// <see cref="ImmutableList{T}"/> and supports <see cref="IDisposable"/> for deterministic cleanup.
16+
/// </summary>
17+
/// <typeparam name="T">The type of elements in the collection.</typeparam>
18+
/// <param name="items">The source enumeration to snapshot into an immutable list.</param>
1319
internal sealed class ReadOnlyDisposableCollection<T>(IEnumerable<T> items) : IReadOnlyCollection<T>, IDisposable
1420
{
21+
/// <summary>
22+
/// The immutable snapshot of the source items.
23+
/// </summary>
1524
private readonly ImmutableList<T> _immutableList = [.. items];
25+
26+
/// <summary>
27+
/// Indicates whether this instance has been disposed.
28+
/// </summary>
1629
private bool _disposedValue;
1730

1831
/// <summary>
@@ -45,7 +58,11 @@ public void Dispose()
4558
GC.SuppressFinalize(this);
4659
}
4760

48-
private void Dispose(bool disposing)
61+
/// <summary>
62+
/// Releases the managed resources used by this instance.
63+
/// </summary>
64+
/// <param name="disposing"><c>true</c> to release managed resources; <c>false</c> when called from a finalizer.</param>
65+
internal void Dispose(bool disposing)
4966
{
5067
if (!_disposedValue)
5168
{
Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2025 ReactiveUI and Contributors. All rights reserved.
1+
// Copyright (c) 2019-2026 ReactiveUI and Contributors. All rights reserved.
22
// Licensed to the ReactiveUI and Contributors under one or more agreements.
33
// The ReactiveUI and Contributors licenses this file to you under the MIT license.
44
// See the LICENSE file in the project root for full license information.
@@ -9,22 +9,41 @@
99

1010
namespace ReactiveUI.Validation.Collections;
1111

12-
internal sealed class SingleValidationText : IValidationText
12+
/// <summary>
13+
/// An <see cref="IValidationText"/> implementation that wraps a single validation message string.
14+
/// </summary>
15+
/// <param name="text">The single validation message string.</param>
16+
internal sealed class SingleValidationText(string text) : IValidationText
1317
{
14-
private readonly string _text;
15-
16-
internal SingleValidationText(string text) => _text = text;
17-
18+
/// <summary>
19+
/// Gets the number of validation messages, which is always 1.
20+
/// </summary>
1821
public int Count => 1;
1922

20-
public string this[int index] => index is 0 ? _text : throw new ArgumentOutOfRangeException(nameof(index));
21-
23+
/// <summary>
24+
/// Gets the validation message at the specified index.
25+
/// </summary>
26+
/// <param name="index">The zero-based index of the message to retrieve.</param>
27+
/// <returns>The validation message string.</returns>
28+
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="index"/> is not 0.</exception>
29+
public string this[int index] => index is 0 ? text : throw new ArgumentOutOfRangeException(nameof(index));
30+
31+
/// <summary>
32+
/// Returns an enumerator that yields the single validation message.
33+
/// </summary>
34+
/// <returns>An enumerator that yields the single validation message.</returns>
2235
public IEnumerator<string> GetEnumerator()
2336
{
24-
yield return _text;
37+
yield return text;
2538
}
2639

40+
/// <inheritdoc/>
2741
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
2842

29-
public string ToSingleLine(string? separator) => _text;
43+
/// <summary>
44+
/// Returns the single validation message as a single-line string.
45+
/// </summary>
46+
/// <param name="separator">Ignored because there is only one message.</param>
47+
/// <returns>The validation message string.</returns>
48+
public string ToSingleLine(string? separator) => text;
3049
}

0 commit comments

Comments
 (0)