Skip to content

Commit 7ff3279

Browse files
authored
feature: Add ValidationRule extensions method overloads to accept any object implementing IValidationState (#148)
* resolves #147 * Added tests for new overloads.
1 parent a9c7104 commit 7ff3279

4 files changed

+136
-1
lines changed

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

+6
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,9 @@ namespace ReactiveUI.Validation.Extensions
223223
where TViewModel : ReactiveUI.IReactiveObject, ReactiveUI.Validation.Abstractions.IValidatableViewModel { }
224224
public static ReactiveUI.Validation.Helpers.ValidationHelper ValidationRule<TViewModel>(this TViewModel viewModel, System.IObservable<bool> validationObservable, string message)
225225
where TViewModel : ReactiveUI.IReactiveObject, ReactiveUI.Validation.Abstractions.IValidatableViewModel { }
226+
public static ReactiveUI.Validation.Helpers.ValidationHelper ValidationRule<TViewModel, TValue>(this TViewModel viewModel, System.IObservable<TValue> validationObservable)
227+
where TViewModel : ReactiveUI.IReactiveObject, ReactiveUI.Validation.Abstractions.IValidatableViewModel
228+
where TValue : ReactiveUI.Validation.States.IValidationState { }
226229
public static ReactiveUI.Validation.Helpers.ValidationHelper ValidationRule<TViewModel, TViewModelProp>(this TViewModel viewModel, System.Linq.Expressions.Expression<System.Func<TViewModel, TViewModelProp>> viewModelProperty, System.IObservable<ReactiveUI.Validation.States.IValidationState> validationObservable)
227230
where TViewModel : ReactiveUI.IReactiveObject, ReactiveUI.Validation.Abstractions.IValidatableViewModel { }
228231
public static ReactiveUI.Validation.Helpers.ValidationHelper ValidationRule<TViewModel, TValue>(this TViewModel viewModel, System.IObservable<TValue> validationObservable, System.Func<TValue, bool> isValidFunc, System.Func<TValue, string> messageFunc)
@@ -249,6 +252,9 @@ namespace ReactiveUI.Validation.Extensions
249252
where TViewModel : ReactiveUI.IReactiveObject, ReactiveUI.Validation.Abstractions.IValidatableViewModel { }
250253
public static ReactiveUI.Validation.Helpers.ValidationHelper ValidationRule<TViewModel, TViewModelProp>(this TViewModel viewModel, System.Linq.Expressions.Expression<System.Func<TViewModel, TViewModelProp>> viewModelProperty, System.IObservable<bool> viewModelObservable, string message)
251254
where TViewModel : ReactiveUI.IReactiveObject, ReactiveUI.Validation.Abstractions.IValidatableViewModel { }
255+
public static ReactiveUI.Validation.Helpers.ValidationHelper ValidationRule<TViewModel, TViewModelProp, TValue>(this TViewModel viewModel, System.Linq.Expressions.Expression<System.Func<TViewModel, TViewModelProp>> viewModelProperty, System.IObservable<TValue> validationObservable)
256+
where TViewModel : ReactiveUI.IReactiveObject, ReactiveUI.Validation.Abstractions.IValidatableViewModel
257+
where TValue : ReactiveUI.Validation.States.IValidationState { }
252258
public static ReactiveUI.Validation.Helpers.ValidationHelper ValidationRule<TViewModel, TViewModelProp, TValue>(this TViewModel viewModel, System.Linq.Expressions.Expression<System.Func<TViewModel, TViewModelProp>> viewModelProperty, System.IObservable<TValue> viewModelObservable, System.Func<TValue, bool> isValidFunc, System.Func<TValue, string> messageFunc)
253259
where TViewModel : ReactiveUI.IReactiveObject, ReactiveUI.Validation.Abstractions.IValidatableViewModel { }
254260
}

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

+6
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,9 @@ namespace ReactiveUI.Validation.Extensions
223223
where TViewModel : ReactiveUI.IReactiveObject, ReactiveUI.Validation.Abstractions.IValidatableViewModel { }
224224
public static ReactiveUI.Validation.Helpers.ValidationHelper ValidationRule<TViewModel>(this TViewModel viewModel, System.IObservable<bool> validationObservable, string message)
225225
where TViewModel : ReactiveUI.IReactiveObject, ReactiveUI.Validation.Abstractions.IValidatableViewModel { }
226+
public static ReactiveUI.Validation.Helpers.ValidationHelper ValidationRule<TViewModel, TValue>(this TViewModel viewModel, System.IObservable<TValue> validationObservable)
227+
where TViewModel : ReactiveUI.IReactiveObject, ReactiveUI.Validation.Abstractions.IValidatableViewModel
228+
where TValue : ReactiveUI.Validation.States.IValidationState { }
226229
public static ReactiveUI.Validation.Helpers.ValidationHelper ValidationRule<TViewModel, TViewModelProp>(this TViewModel viewModel, System.Linq.Expressions.Expression<System.Func<TViewModel, TViewModelProp>> viewModelProperty, System.IObservable<ReactiveUI.Validation.States.IValidationState> validationObservable)
227230
where TViewModel : ReactiveUI.IReactiveObject, ReactiveUI.Validation.Abstractions.IValidatableViewModel { }
228231
public static ReactiveUI.Validation.Helpers.ValidationHelper ValidationRule<TViewModel, TValue>(this TViewModel viewModel, System.IObservable<TValue> validationObservable, System.Func<TValue, bool> isValidFunc, System.Func<TValue, string> messageFunc)
@@ -249,6 +252,9 @@ namespace ReactiveUI.Validation.Extensions
249252
where TViewModel : ReactiveUI.IReactiveObject, ReactiveUI.Validation.Abstractions.IValidatableViewModel { }
250253
public static ReactiveUI.Validation.Helpers.ValidationHelper ValidationRule<TViewModel, TViewModelProp>(this TViewModel viewModel, System.Linq.Expressions.Expression<System.Func<TViewModel, TViewModelProp>> viewModelProperty, System.IObservable<bool> viewModelObservable, string message)
251254
where TViewModel : ReactiveUI.IReactiveObject, ReactiveUI.Validation.Abstractions.IValidatableViewModel { }
255+
public static ReactiveUI.Validation.Helpers.ValidationHelper ValidationRule<TViewModel, TViewModelProp, TValue>(this TViewModel viewModel, System.Linq.Expressions.Expression<System.Func<TViewModel, TViewModelProp>> viewModelProperty, System.IObservable<TValue> validationObservable)
256+
where TViewModel : ReactiveUI.IReactiveObject, ReactiveUI.Validation.Abstractions.IValidatableViewModel
257+
where TValue : ReactiveUI.Validation.States.IValidationState { }
252258
public static ReactiveUI.Validation.Helpers.ValidationHelper ValidationRule<TViewModel, TViewModelProp, TValue>(this TViewModel viewModel, System.Linq.Expressions.Expression<System.Func<TViewModel, TViewModelProp>> viewModelProperty, System.IObservable<TValue> viewModelObservable, System.Func<TValue, bool> isValidFunc, System.Func<TValue, string> messageFunc)
253259
where TViewModel : ReactiveUI.IReactiveObject, ReactiveUI.Validation.Abstractions.IValidatableViewModel { }
254260
}

src/ReactiveUI.Validation.Tests/ValidationBindingTests.cs

+49-1
Original file line numberDiff line numberDiff line change
@@ -659,7 +659,7 @@ public void ShouldUpdateValidationHelperBindingOnPropertyChange()
659659
}
660660

661661
/// <summary>
662-
/// Verifies that the ValidationRule(IValidationState) methods work.
662+
/// Verifies that the <see cref="ValidatableViewModelExtensions.ValidationRule{TVIewModel}(TVIewModel, IObservable{IValidationState})"/> methods work.
663663
/// </summary>
664664
[Fact]
665665
public void ShouldBindValidationRuleEmittingValidationStates()
@@ -671,6 +671,54 @@ public void ShouldBindValidationRuleEmittingValidationStates()
671671
var isViewModelBlocked = new ReplaySubject<bool>(1);
672672
isViewModelBlocked.OnNext(true);
673673

674+
// Create IObservable<IValidationState>
675+
var nameValidationState = view.ViewModel.WhenAnyValue(
676+
vm => vm.Name,
677+
name => (IValidationState)new CustomValidationState(
678+
!string.IsNullOrWhiteSpace(name),
679+
nameErrorMessage));
680+
681+
view.ViewModel.ValidationRule(
682+
viewModel => viewModel.Name,
683+
nameValidationState);
684+
685+
var viewModelBlockedValidationState = isViewModelBlocked.Select(blocked =>
686+
(IValidationState)new CustomValidationState(!blocked, viewModelIsBlockedMessage));
687+
688+
view.ViewModel.ValidationRule(viewModelBlockedValidationState);
689+
690+
view.Bind(view.ViewModel, x => x.Name, x => x.NameLabel);
691+
view.BindValidation(view.ViewModel, x => x.Name, x => x.NameErrorLabel);
692+
view.BindValidation(view.ViewModel, x => x.NameErrorContainer.Text);
693+
694+
Assert.Equal(2, view.ViewModel.ValidationContext.Validations.Count);
695+
Assert.False(view.ViewModel.ValidationContext.IsValid);
696+
Assert.Contains(nameErrorMessage, view.NameErrorLabel, comparison);
697+
Assert.Contains(viewModelIsBlockedMessage, view.NameErrorContainer.Text, comparison);
698+
699+
view.ViewModel.Name = "Qwerty";
700+
isViewModelBlocked.OnNext(false);
701+
702+
Assert.Equal(2, view.ViewModel.ValidationContext.Validations.Count);
703+
Assert.True(view.ViewModel.ValidationContext.IsValid);
704+
Assert.DoesNotContain(nameErrorMessage, view.NameErrorLabel, comparison);
705+
Assert.DoesNotContain(viewModelIsBlockedMessage, view.NameErrorContainer.Text, comparison);
706+
}
707+
708+
/// <summary>
709+
/// Verifies that the <see cref="ValidatableViewModelExtensions.ValidationRule{TVIewModel, TValue}(TVIewModel, IObservable{TValue})"/> methods work.
710+
/// </summary>
711+
[Fact]
712+
public void ShouldBindValidationRuleEmittingValidationStatesGeneric()
713+
{
714+
const StringComparison comparison = StringComparison.InvariantCulture;
715+
const string viewModelIsBlockedMessage = "View model is blocked.";
716+
const string nameErrorMessage = "Name shouldn't be empty.";
717+
var view = new TestView(new TestViewModel { Name = string.Empty });
718+
var isViewModelBlocked = new ReplaySubject<bool>(1);
719+
isViewModelBlocked.OnNext(true);
720+
721+
// Use the observable directly in the rules, which use the generic version of the ex
674722
view.ViewModel.ValidationRule(
675723
viewModel => viewModel.Name,
676724
view.ViewModel.WhenAnyValue(

src/ReactiveUI.Validation/Extensions/ValidatableViewModelExtensions.cs

+75
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Linq;
99
using System.Linq.Expressions;
1010
using System.Reactive.Disposables;
11+
using System.Reactive.Linq;
1112
using ReactiveUI.Validation.Abstractions;
1213
using ReactiveUI.Validation.Components;
1314
using ReactiveUI.Validation.Components.Abstractions;
@@ -227,6 +228,39 @@ public static ValidationHelper ValidationRule<TViewModel>(
227228
validationObservable));
228229
}
229230

231+
/// <summary>
232+
/// Setup a validation rule with a general observable based on <see cref="IValidationState"/>.
233+
/// </summary>
234+
/// <typeparam name="TViewModel">ViewModel type.</typeparam>
235+
/// <typeparam name="TValue">Validation observable type.</typeparam>
236+
/// <param name="viewModel">ViewModel instance.</param>
237+
/// <param name="validationObservable">Observable to define if the viewModel is valid or not.</param>
238+
/// <returns>Returns a <see cref="ValidationHelper"/> object.</returns>
239+
/// <remarks>
240+
/// It should be noted that the observable should provide an initial value, otherwise that can result
241+
/// in an inconsistent performance.
242+
/// </remarks>
243+
public static ValidationHelper ValidationRule<TViewModel, TValue>(
244+
this TViewModel viewModel,
245+
IObservable<TValue> validationObservable)
246+
where TViewModel : IReactiveObject, IValidatableViewModel
247+
where TValue : IValidationState
248+
{
249+
if (viewModel is null)
250+
{
251+
throw new ArgumentNullException(nameof(viewModel));
252+
}
253+
254+
if (validationObservable is null)
255+
{
256+
throw new ArgumentNullException(nameof(validationObservable));
257+
}
258+
259+
return viewModel.RegisterValidation(
260+
new ObservableValidation<TViewModel, bool>(
261+
validationObservable.Select(s => s as IValidationState)));
262+
}
263+
230264
/// <summary>
231265
/// Setup a validation rule with a general observable indicating validity and a static error message
232266
/// for the given view model property.
@@ -372,6 +406,47 @@ public static ValidationHelper ValidationRule<TViewModel, TViewModelProp>(
372406
viewModelProperty, validationObservable));
373407
}
374408

409+
/// <summary>
410+
/// Setup a validation rule with a general observable based on <see cref="IValidationState"/>.
411+
/// </summary>
412+
/// <typeparam name="TViewModel">ViewModel type.</typeparam>
413+
/// <typeparam name="TViewModelProp">ViewModel property type.</typeparam>
414+
/// <typeparam name="TValue">Validation observable type.</typeparam>
415+
/// <param name="viewModel">ViewModel instance.</param>
416+
/// <param name="viewModelProperty">ViewModel property referenced in viewModelObservableProperty.</param>
417+
/// <param name="validationObservable">Observable to define if the viewModel is valid or not.</param>
418+
/// <returns>Returns a <see cref="ValidationHelper"/> object.</returns>
419+
/// <remarks>
420+
/// It should be noted that the observable should provide an initial value, otherwise that can result
421+
/// in an inconsistent performance.
422+
/// </remarks>
423+
public static ValidationHelper ValidationRule<TViewModel, TViewModelProp, TValue>(
424+
this TViewModel viewModel,
425+
Expression<Func<TViewModel, TViewModelProp>> viewModelProperty,
426+
IObservable<TValue> validationObservable)
427+
where TViewModel : IReactiveObject, IValidatableViewModel
428+
where TValue : IValidationState
429+
{
430+
if (viewModel is null)
431+
{
432+
throw new ArgumentNullException(nameof(viewModel));
433+
}
434+
435+
if (viewModelProperty is null)
436+
{
437+
throw new ArgumentNullException(nameof(viewModelProperty));
438+
}
439+
440+
if (validationObservable is null)
441+
{
442+
throw new ArgumentNullException(nameof(validationObservable));
443+
}
444+
445+
return viewModel.RegisterValidation(
446+
new ObservableValidation<TViewModel, bool, TViewModelProp>(
447+
viewModelProperty, validationObservable.Select(v => v as IValidationState)));
448+
}
449+
375450
/// <summary>
376451
/// Clears the validation rules associated with teh specified property.
377452
/// </summary>

0 commit comments

Comments
 (0)