Skip to content

Commit 4b6ad22

Browse files
authored
feature: Custom Formatters in ReactiveValidationObject (#154)
* Custom formatters in ReactiveValidationObject * Approve the API * Test custom formatters in ReactiveValidationObject
1 parent d0c5c8f commit 4b6ad22

5 files changed

+91
-15
lines changed

src/ReactiveUI.Validation.Tests/API/ApiApprovalTests.ValidationProject.net472.approved.txt

+2-2
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ namespace ReactiveUI.Validation.Helpers
321321
{
322322
public abstract class ReactiveValidationObject : ReactiveUI.ReactiveObject, ReactiveUI.Validation.Abstractions.IValidatableViewModel, System.ComponentModel.INotifyDataErrorInfo
323323
{
324-
protected ReactiveValidationObject(System.Reactive.Concurrency.IScheduler? scheduler = null) { }
324+
protected ReactiveValidationObject(System.Reactive.Concurrency.IScheduler? scheduler = null, ReactiveUI.Validation.Formatters.Abstractions.IValidationTextFormatter<string>? formatter = null) { }
325325
public bool HasErrors { get; }
326326
public ReactiveUI.Validation.Contexts.ValidationContext ValidationContext { get; }
327327
public event System.EventHandler<System.ComponentModel.DataErrorsChangedEventArgs>? ErrorsChanged;
@@ -333,7 +333,7 @@ namespace ReactiveUI.Validation.Helpers
333333
"veValidationObject.")]
334334
public abstract class ReactiveValidationObject<TViewModel> : ReactiveUI.Validation.Helpers.ReactiveValidationObject
335335
{
336-
protected ReactiveValidationObject(System.Reactive.Concurrency.IScheduler? scheduler = null) { }
336+
protected ReactiveValidationObject(System.Reactive.Concurrency.IScheduler? scheduler = null, ReactiveUI.Validation.Formatters.Abstractions.IValidationTextFormatter<string>? formatter = null) { }
337337
}
338338
public class ValidationHelper : ReactiveUI.ReactiveObject, System.IDisposable
339339
{

src/ReactiveUI.Validation.Tests/API/ApiApprovalTests.ValidationProject.netcoreapp3.1.approved.txt

+2-2
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ namespace ReactiveUI.Validation.Helpers
321321
{
322322
public abstract class ReactiveValidationObject : ReactiveUI.ReactiveObject, ReactiveUI.Validation.Abstractions.IValidatableViewModel, System.ComponentModel.INotifyDataErrorInfo
323323
{
324-
protected ReactiveValidationObject(System.Reactive.Concurrency.IScheduler? scheduler = null) { }
324+
protected ReactiveValidationObject(System.Reactive.Concurrency.IScheduler? scheduler = null, ReactiveUI.Validation.Formatters.Abstractions.IValidationTextFormatter<string>? formatter = null) { }
325325
public bool HasErrors { get; }
326326
public ReactiveUI.Validation.Contexts.ValidationContext ValidationContext { get; }
327327
public event System.EventHandler<System.ComponentModel.DataErrorsChangedEventArgs>? ErrorsChanged;
@@ -333,7 +333,7 @@ namespace ReactiveUI.Validation.Helpers
333333
"veValidationObject.")]
334334
public abstract class ReactiveValidationObject<TViewModel> : ReactiveUI.Validation.Helpers.ReactiveValidationObject
335335
{
336-
protected ReactiveValidationObject(System.Reactive.Concurrency.IScheduler? scheduler = null) { }
336+
protected ReactiveValidationObject(System.Reactive.Concurrency.IScheduler? scheduler = null, ReactiveUI.Validation.Formatters.Abstractions.IValidationTextFormatter<string>? formatter = null) { }
337337
}
338338
public class ValidationHelper : ReactiveUI.ReactiveObject, System.IDisposable
339339
{

src/ReactiveUI.Validation.Tests/Models/IndeiTestViewModel.cs

+10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// See the LICENSE file in the project root for full license information.
55

66
using System.Reactive.Concurrency;
7+
using ReactiveUI.Validation.Formatters.Abstractions;
78
using ReactiveUI.Validation.Helpers;
89

910
namespace ReactiveUI.Validation.Tests.Models
@@ -24,6 +25,15 @@ public IndeiTestViewModel()
2425
{
2526
}
2627

28+
/// <summary>
29+
/// Initializes a new instance of the <see cref="IndeiTestViewModel"/> class.
30+
/// </summary>
31+
/// <param name="formatter">Validation text formatter.</param>
32+
public IndeiTestViewModel(IValidationTextFormatter<string> formatter)
33+
: base(ImmediateScheduler.Instance, formatter)
34+
{
35+
}
36+
2737
/// <summary>
2838
/// Gets or sets get the Name.
2939
/// </summary>

src/ReactiveUI.Validation.Tests/NotifyDataErrorInfoTests.cs

+40
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
using System.ComponentModel;
88
using System.Linq;
99
using System.Reactive.Linq;
10+
using ReactiveUI.Validation.Collections;
1011
using ReactiveUI.Validation.Components;
1112
using ReactiveUI.Validation.Extensions;
13+
using ReactiveUI.Validation.Formatters.Abstractions;
1214
using ReactiveUI.Validation.Helpers;
1315
using ReactiveUI.Validation.Tests.Models;
1416
using Xunit;
@@ -258,5 +260,43 @@ public void ShouldDetachAndDisposeTheComponentWhenValidationHelperDisposes()
258260
Assert.Equal(2, arguments.Count);
259261
Assert.Equal(nameof(view.ViewModel.Name), arguments[1].PropertyName);
260262
}
263+
264+
/// <summary>
265+
/// Verifies that we support custom formatters in our <see cref="INotifyDataErrorInfo"/> implementation.
266+
/// </summary>
267+
[Fact]
268+
public void ShouldInvokeCustomFormatters()
269+
{
270+
var formatter = new PrefixFormatter("Validation error:");
271+
var view = new IndeiTestView(new IndeiTestViewModel(formatter) { Name = string.Empty });
272+
var arguments = new List<DataErrorsChangedEventArgs>();
273+
274+
view.ViewModel.ErrorsChanged += (sender, args) => arguments.Add(args);
275+
view.ViewModel.ValidationRule(
276+
viewModel => viewModel.Name,
277+
name => !string.IsNullOrWhiteSpace(name),
278+
"Name shouldn't be empty.");
279+
280+
Assert.Equal(1, view.ViewModel.ValidationContext.Validations.Count);
281+
Assert.False(view.ViewModel.ValidationContext.IsValid);
282+
Assert.True(view.ViewModel.HasErrors);
283+
284+
var errors = view.ViewModel
285+
.GetErrors("Name")
286+
.Cast<string>()
287+
.ToArray();
288+
289+
Assert.Single(errors);
290+
Assert.Equal("Validation error: Name shouldn't be empty.", errors[0]);
291+
}
292+
293+
private class PrefixFormatter : IValidationTextFormatter<string>
294+
{
295+
private readonly string _prefix;
296+
297+
public PrefixFormatter(string prefix) => _prefix = prefix;
298+
299+
public string Format(ValidationText validationText) => $"{_prefix} {validationText.ToSingleLine()}";
300+
}
261301
}
262302
}

src/ReactiveUI.Validation/Helpers/ReactiveValidationObject.cs

+37-11
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
using ReactiveUI.Validation.Collections;
1818
using ReactiveUI.Validation.Components.Abstractions;
1919
using ReactiveUI.Validation.Contexts;
20+
using ReactiveUI.Validation.Formatters;
21+
using ReactiveUI.Validation.Formatters.Abstractions;
2022
using ReactiveUI.Validation.States;
23+
using Splat;
2124

2225
namespace ReactiveUI.Validation.Helpers
2326
{
@@ -34,9 +37,18 @@ public abstract class ReactiveValidationObject<TViewModel> : ReactiveValidationO
3437
/// <summary>
3538
/// Initializes a new instance of the <see cref="ReactiveValidationObject{TViewModel}"/> class.
3639
/// </summary>
37-
/// <param name="scheduler">Scheduler for OAPHs and for the the ValidationContext.</param>
38-
protected ReactiveValidationObject(IScheduler? scheduler = null)
39-
: base(scheduler)
40+
/// <param name="scheduler">
41+
/// Scheduler for the <see cref="ValidationContext"/>. Uses <see cref="CurrentThreadScheduler"/> by default.
42+
/// </param>
43+
/// <param name="formatter">
44+
/// Validation formatter. Defaults to <see cref="SingleLineFormatter"/>. In order to override the global
45+
/// default value, implement <see cref="IValidationTextFormatter{TOut}"/> and register an instance of
46+
/// IValidationTextFormatter&lt;string&gt; into Splat.Locator.
47+
/// </param>
48+
protected ReactiveValidationObject(
49+
IScheduler? scheduler = null,
50+
IValidationTextFormatter<string>? formatter = null)
51+
: base(scheduler, formatter)
4052
{
4153
}
4254
}
@@ -47,14 +59,28 @@ protected ReactiveValidationObject(IScheduler? scheduler = null)
4759
public abstract class ReactiveValidationObject : ReactiveObject, IValidatableViewModel, INotifyDataErrorInfo
4860
{
4961
private readonly HashSet<string> _mentionedPropertyNames = new HashSet<string>();
62+
private readonly IValidationTextFormatter<string> _formatter;
5063
private bool _hasErrors;
5164

5265
/// <summary>
5366
/// Initializes a new instance of the <see cref="ReactiveValidationObject"/> class.
5467
/// </summary>
55-
/// <param name="scheduler">Scheduler for OAPHs and for the the ValidationContext.</param>
56-
protected ReactiveValidationObject(IScheduler? scheduler = null)
68+
/// <param name="scheduler">
69+
/// Scheduler for the <see cref="ValidationContext"/>. Uses <see cref="CurrentThreadScheduler"/> by default.
70+
/// </param>
71+
/// <param name="formatter">
72+
/// Validation formatter. Defaults to <see cref="SingleLineFormatter"/>. In order to override the global
73+
/// default value, implement <see cref="IValidationTextFormatter{TOut}"/> and register an instance of
74+
/// IValidationTextFormatter&lt;string&gt; into Splat.Locator.
75+
/// </param>
76+
protected ReactiveValidationObject(
77+
IScheduler? scheduler = null,
78+
IValidationTextFormatter<string>? formatter = null)
5779
{
80+
_formatter = formatter ??
81+
Locator.Current.GetService<IValidationTextFormatter<string>>() ??
82+
SingleLineFormatter.Default;
83+
5884
ValidationContext = new ValidationContext(scheduler);
5985
ValidationContext.Validations
6086
.ToObservableChangeSet()
@@ -89,13 +115,13 @@ public bool HasErrors
89115
/// <returns>A list of error messages, usually strings.</returns>
90116
/// <inheritdoc />
91117
public virtual IEnumerable GetErrors(string propertyName) =>
92-
string.IsNullOrEmpty(propertyName) ?
93-
SelectInvalidPropertyValidations()
94-
.SelectMany(validation => validation.Text ?? ValidationText.None)
95-
.ToArray() :
96-
SelectInvalidPropertyValidations()
118+
string.IsNullOrEmpty(propertyName)
119+
? SelectInvalidPropertyValidations()
120+
.Select(state => _formatter.Format(state.Text ?? ValidationText.None))
121+
.ToArray()
122+
: SelectInvalidPropertyValidations()
97123
.Where(validation => validation.ContainsPropertyName(propertyName))
98-
.SelectMany(validation => validation.Text ?? ValidationText.None)
124+
.Select(state => _formatter.Format(state.Text ?? ValidationText.None))
99125
.ToArray();
100126

101127
/// <summary>

0 commit comments

Comments
 (0)