reactiveui / reactiveui.validation Goto Github PK
View Code? Open in Web Editor NEWValidation helpers for ReactiveUI-based apps.
Home Page: https://reactiveui.net/docs/handbook/user-input-validation
License: MIT License
Validation helpers for ReactiveUI-based apps.
Home Page: https://reactiveui.net/docs/handbook/user-input-validation
License: MIT License
Describe the bug
When I change the property value in the ViewModel with the ReactiveValidationObject<>
the validation works wrongly on TextBox
with NotifyOnValidationError=True, ValidatesOnDataErrors=True
. When I change TextBox property directly via keyboard its working perfectly.
When I started to debug, I noticed that the ValidationContext has not up to date information.
Steps To Reproduce
I created a repository with the bug https://github.com/ScarletKuro/ValidationContextBug
It has ReactiveValidationObjectBugViewModel with the bug, and the ClassicValidationViewModel that implements IDataErrorInfo
and works correctly, just to make sure the bug is not related with something else. I know that ReactiveValidationObject
implements the INotifyDataErrorInfo, but it still should work the same, because if I make IDataErrorInfo to work with ValidationContex it gets the same buggy behaviour.
Provide the steps to reproduce the behavior:
Simple code
ViewModel:
public class ReactiveValidationObjectBugViewModel : ReactiveValidationObject<ReactiveValidationObjectBugViewModel>
{
[Reactive]
public string CustomerCode { get; set; } = string.Empty;
public ReactiveCommand<Unit, string> Numeric0Command { get; }
public ReactiveCommand<Unit, string> Clear { get; }
public ReactiveValidationObjectBugViewModel() : base(Scheduler.Immediate)
{
Numeric0Command = ReactiveCommand.Create(() => CustomerCode += 0);
Clear = ReactiveCommand.Create(() => CustomerCode = string.Empty);
this.ValidationRule(viewModel => viewModel.CustomerCode,
customerCode => !string.IsNullOrEmpty(customerCode), "You need to enter customer code");
}
}
View xaml:
<TextBox Text="{Binding CustomerCode, UpdateSourceTrigger=PropertyChanged, NotifyOnValidationError=True, ValidatesOnDataErrors=True, Mode=TwoWay}"/>
Current buggy behavior
As you can notice, when we have a value the Text is red. When the TextBox is empty its not red. But when I type more then it gets updated.
Environment
Might be worth considering changing this to Version="9.*'
-- this will target the latest 9 series of RxUI given you're fairly tightly bound to RxUI.
Still means you have to do a build/nuget release whenever you want to target a new version but just means you'll always be targetting the latest when you do so.
Describe the bug
The readme says has tells you that for INotifyDataErrorInfo you can inherit ReactiveValidationObject and call this.ValidationRule to add validation rules.
However ReactiveValidationObject doesn't have a ValidationRule method and so the example does not compile:
It can be seen from https://github.com/reactiveui/ReactiveUI.Validation/blob/main/src/ReactiveUI.Validation/Helpers/ReactiveValidationObject.cs which doesn't have a method called ValidationRule
public class SampleViewModel : ReactiveValidationObject
{
public SampleViewModel()
{
// this line from the readme doesn't compile because ValidationRule doesn't exist.
this.ValidationRule(
viewModel => viewModel.Name,
name => !string.IsNullOrWhiteSpace(name),
"Name shouldn't be null or white space.");
}
private string _name = string.Empty;
public string Name
{
get => _name;
set => this.RaiseAndSetIfChanged(ref _name, value);
}
}
Steps To Reproduce
Us the sample from the readme
-> notice it doesn't compile because it uses a method that doesn't exist.
Expected behavior
I should be able to follow the readme and have a working validation example
Environment
Describe the bug
If you call BindValidation inside WhenActivated, it throws an exception on app start for both ios and android
this.WhenActivated(d =>
{
// Exception here
this.BindValidation(
ViewModel,
vm => vm.Password,
page => page.validationLabel.Text).DisposeWith(d);
});
here validationLabel is a Xamarin.Form Label
Error/Exception
page.validationLabel.Text Binding received an Exception! - System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection.
Steps To Reproduce
Same as the example code in ReadMe
Environment
Version: Xamarin (4.6.0.726) ReactiveUI.XamForms (11.3.8) ReactiveUI.Validation (1.4.13)
I'm trying the samples adapted to a WPF project.
I copied the ViewModel sample as-is and created a WPF view with type-safe bindings like the given android sample.
ViewModel:
public class SampleViewModel : ReactiveObject, ISupportsValidation
{
public SampleViewModel()
{
// Name must be at least 3 chars. The selector is the property name and the line below is
// a single property validator.
this.ValidationRule(
viewModel => viewModel.Name,
name => !string.IsNullOrWhiteSpace(name),
"You must specify a valid name");
// Age must be between 13 and 100, message includes the silly length being passed in,
// stored in a property of the ViewModel.
AgeRule = this.ValidationRule(
viewModel => viewModel.Age,
age => age >= 13 && age <= 100,
age => $"{age} is a silly age");
var nameAndAgeValid = this
.WhenAnyValue(x => x.Age, x => x.Name, (age, name) => new { Age = age, Name = name })
.Select(x => x.Age > 10 && !string.IsNullOrEmpty(x.Name));
// Create a rule from an IObservable.
ComplexRule = this.ValidationRule(
_ => nameAndAgeValid,
(vm, state) => !state ? "That's a ridiculous name / age combination" : string.Empty);
// IsValid extension method returns true when all validations succeed.
var canSave = this.IsValid();
// Save command is only active when all validators are valid.
Save = ReactiveCommand.CreateFromTask(async unit => { }, canSave);
}
[Reactive] public int Age { get; set; }
// Declare a separate validator for age rule.
public ValidationHelper AgeRule { get; }
// Declare a separate validator for complex rule.
public ValidationHelper ComplexRule { get; }
[Reactive] public string Name { get; set; }
public ReactiveCommand<Unit, Unit> Save { get; }
// Initialize validation context that manages reactive validations.
public ValidationContext ValidationContext { get; } = new ValidationContext();
}
View:
<rxui:ReactiveWindow x:Class="Sncf.RealTimeTrain.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:rxui="http://reactiveui.net"
xmlns:vms="clr-namespace:Sncf.RealTimeTrain.ViewModels;assembly=Sncf.RealTimeTrain.Shared"
d:DataContext="{d:DesignInstance Type=vms:SampleViewModel}"
d:DesignHeight="450"
d:DesignWidth="800"
x:TypeArguments="vms:SampleViewModel"
mc:Ignorable="d">
<StackPanel Margin="20">
<TextBox x:Name="NameTextBox" Margin="5" />
<TextBlock x:Name="ErrorsTextBlock" Margin="5" />
</StackPanel>
</rxui:ReactiveWindow>
public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, vm => vm.Name, view => view.NameTextBox.Text);
// This works fine
this.BindValidation(ViewModel, view => view.ErrorsTextBlock.Text);
// This fails with a NullReferenceException
this.BindValidation(ViewModel, vm => vm.Name, view => view.ErrorsTextBlock.Text);
});
}
}
I'm using the latest stable version of ReactiveUI 9.12.1 and the latest validation version 1.0.3:
<PackageReference Include="ReactiveUI" Version="9.12.1" />
<PackageReference Include="ReactiveUI.Fody" Version="9.12.1" />
<PackageReference Include="ReactiveUI.Validation" Version="1.0.3" />
Also with the one that works, every validation message is appended to each other without a NewLine or anything. How do I customize this? I can open another issue if needed.
Currently, I have some code that parses a URL and validates it. Obviously, this can be a slow process so is done asynchronously. A simplified version can be thought of as producing an IObservable<string>
where the string represents an error if non-null.
It would be nice to add overloads to the ValidationRule
extension method to accept an Observable<T>
and 2 functions:
Func<T, bool>
, to determine if the item of type T
is valid.Func<T, string>
, to return the corresponding error message based on the item.The ensures that there are no race conditions between evaluating the validity of the item, and its message. In our example, these functions would be:
An alternative is to define an interface IValidationState
(which is implemented by ValidationState
), but only defines IsValid
and Text
. The ValidationRule can then accept IObservable<IValidationState>
and doesn't need any extractor functions. This can form the base class of the existing implementations.
Example:
I used the following implementation (note the difficulty of implementing such extensions due to IValidatesProperties<TViewModel>.ContainsProperty<TProp>(Expression<Func<TViewModel, TProp>> propertyExpression, bool exclusively = false)
and the internal nature of the GetPropertyPath()
extension method (I inlined in this implementation). Further, I specify associated properties on creation (rather than using an AddProperty
method)
/// <inheritdoc cref="ReactiveObject" />
/// <inheritdoc cref="IDisposable" />
/// <inheritdoc cref="IPropertyValidationComponent{TViewModel}" />
/// <summary>
/// More generic observable for determination of validity.
/// </summary>
/// <remarks>
/// We probably need a more 'complex' one, where the params of the validation block are
/// passed through?
/// Also, what about access to the view model to output the error message?.
/// </remarks>
public class ObservableValidationRule<TViewModel, T> : ReactiveObject, IDisposable, IPropertyValidationComponent<TViewModel>
{
[SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Disposed by field _disposables.")]
private readonly ReplaySubject<ValidationState> _lastValidationStateSubject =
new ReplaySubject<ValidationState>(1);
/// <summary>
/// The list of property names this validator is referencing.
/// </summary>
private readonly HashSet<string> _propertyNames;
// the underlying connected observable for the validation change which is published
private readonly IConnectableObservable<ValidationState> _validityConnectedObservable;
private readonly CompositeDisposable _disposables = new CompositeDisposable();
private bool _isActive;
private bool _isValid;
private ValidationText? _text;
/// <summary>
/// Initializes a new instance of the <see cref="ModelObservableValidationBase{TViewModel}"/> class.
/// </summary>
/// <param name="validityObservable">ViewModel instance.</param>
public ObservableValidationRule(
IObservable<T> validityObservable,
Func<T, bool> isValid,
Func<T, ValidationText> validationText,
params string[] properties)
{
_isValid = true;
_text = new ValidationText();
_disposables.Add(_lastValidationStateSubject.Do(s =>
{
_isValid = s.IsValid;
_text = s.Text;
}).Subscribe());
_propertyNames = new HashSet<string>(properties);
_validityConnectedObservable = Observable.Defer(() => validityObservable)
.Select(item => new ValidationState(isValid(item), validationText(item), this))
.Multicast(_lastValidationStateSubject);
}
/// <inheritdoc/>
public int PropertyCount => _propertyNames.Count;
/// <inheritdoc/>
public IEnumerable<string> Properties => _propertyNames.AsEnumerable();
/// <inheritdoc/>
public ValidationText? Text
{
get
{
Activate();
return _text;
}
}
/// <inheritdoc/>
public bool IsValid
{
get
{
Activate();
return _isValid;
}
}
/// <inheritdoc/>
public IObservable<ValidationState> ValidationStatusChange
{
get
{
Activate();
return _validityConnectedObservable;
}
}
/// <inheritdoc/>
public void Dispose()
{
// Dispose of unmanaged resources.
Dispose(true);
// Suppress finalization.
GC.SuppressFinalize(this);
}
/// <inheritdoc/>
public bool ContainsProperty<TProp>(Expression<Func<TViewModel, TProp>> property, bool exclusively = false)
{
if (property is null)
{
throw new ArgumentNullException(nameof(property));
}
var expression = property.Body;
var path = new StringBuilder();
while (expression is MemberExpression memberExpression)
{
if (path.Length > 0)
{
path.Insert(0, '.');
}
path.Insert(0, memberExpression.Member.Name);
expression = memberExpression.Expression;
}
var propertyName = path.ToString();
return ContainsPropertyName(propertyName, exclusively);
}
/// <inheritdoc/>
public bool ContainsPropertyName(string propertyName, bool exclusively = false) =>
exclusively
? _propertyNames.Contains(propertyName) && _propertyNames.Count == 1
: _propertyNames.Contains(propertyName);
/// <summary>
/// Disposes of the managed resources.
/// </summary>
/// <param name="disposing">If its getting called by the <see cref="BasePropertyValidation{TViewModel}.Dispose()"/> method.</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_disposables?.Dispose();
}
}
private void Activate()
{
if (_isActive)
{
return;
}
_isActive = true;
_disposables.Add(_validityConnectedObservable.Connect());
}
}
And here is an example of it in use (without the extension method), parsedAddress
is an Observable<ParsedAddress>
which contain information regarding an entered address that fires sometime after it is entered (there's also a 200ms Throttle
involved in its creation).
var addressRule = new ObservableValidationRule<CameraViewModel, ParsedAddress>(
parsedAddress,
parsed => string.IsNullOrWhiteSpace(parsed.Address) || parsed.EndPoint != null,
parsed => new ValidationText(parsed.Message ?? string.Empty),
nameof(Address));
ValidationContext.Add(addressRule);
AddressRule = new ValidationHelper(addressRule);
Describe the issue
When using ReactiveUI.Validation in a WPF application, it works as expected if using manual startup (window.Show or windows.ShowDialog) but if using StartUri, the first validation rule is applied/notify when windows is shown which feels like awkward UX.
Steps To Reproduce
I put the sample code in a repo https://github.com/gerardo-lijs/ReactiveUI-Validation-Sample
The last commit in master branch shows how it works with Startup manual and the previous commit how it behaves with StartUri
Expected behavior
Not sure if this is something that can be fixed in ReactiveUI.Validation or it's something in the way WPF starts the window with StartUri. Maybe just mentioning this in the docs is enough so that other developers avoid StartUri if they want to use ReactiveUI.Validation.
**Extra comment
Another user in github @kagerouttepaso found that if the ViewModel uses the TaskpoolScheduler
public MainViewModel() : base(RxApp.TaskpoolScheduler) then every validation rule is applied/notify when Window is Shown.
Describe the bug
When calling this.BindValidation on a ReactiveUserControl, it throws the exception:
System.NotSupportedExceptionย : 'Index expressions are only supported with constants.'
Steps To Reproduce
Follow: https://www.reactiveui.net/docs/handbook/user-input-validation/
And at the end, the bindValidation will throw an error.
Expected behavior
It should not throw an error and just bind correctly
Code
ConnectView:
public partial class ConnectView : ReactiveUserControl<ConnectViewModel> {
public ConnectView() {
InitializeComponent();
// Input validation
this.WhenActivated(disposables => {
this.BindValidation(ViewModel, viewModel => viewModel.Username, view => view.FindControl<TextBlock>("UsernameError").Text).DisposeWith(disposables);
});
}
private void InitializeComponent() {
AvaloniaXamlLoader.Load(this);
}
}
ConnectViewModel:
public class ConnectViewModel : ReactiveObject, IValidatableViewModel {
public ValidationContext ValidationContext { get; } = new ValidationContext();
[Reactive] public string Username { get; set; } = "";
[Reactive] public string Password { get; set; } = "";
public ConnectViewModel() {
// Username cannot be empty
this.ValidationRule(viewModel => viewModel.Username, username => !string.IsNullOrEmpty(username), "Username cannot be empty..");
}
}
And of course I have a TextBlock with the x:Name attribute with a value of "UsernameError".
Additional context
I am using Avalonia.
Thanks for your help!
Hello, firstly thanks for your great project.
It would be great if validation would support dynamic validation rules change: for example, i do deed to configure some hardware, different options are available for different models, for ex. "Model A" can be connected either via TCP/IP or via RS232, "Model B" supports RS232 only, so for the first TCP/IP settings required, for the second - RS232.
So i do need smth like:
this.WhenAnyValue(_ => _.Model)
.Subscribe(m =>
{
this.ClearValidationRules(_ => _.IpAddress);
this.ClearValidationRules(_ => _.Rs232Port);
//...
if (m == Model.A)
this.ValidationRule(_ => _.IpAddress, _ => !string.IsNullOrEmpty(_), "IP address required");
//...
}
)
Is your feature request related to a problem? Please describe.
Currently, codecov reports the following coverage percentage for ReactiveUI.Validation:
Describe the solution you'd like
Let's increase coverage to ensure further pull requests won't break existing API etc.
Probably ~40-50% will be fine enough, but higher coverage is better.
Describe suggestions on how to achieve the feature
Let's write more tests!
Hi,
I'm playing with validation using INotifyDataErrorInfo
and I'm very confused and I don't know how to use it properly. I couldn't find concrete info in documentation, only sample project gives me some clues, but I don't know if it is all what I should know.
Let's say that I want to make a TextBox
and default ErrorTemplate (red border) should be visible if value is empty. Based on code from sample I'm suspecting that I have to:
TextBox
Text
to view-model using regular XAML binding,ViewModel
to DataContext
.So in short - is there another way to use INotifyDataErrorInfo
using only ReactiveUi bindings? Unfortunately I can't turn on INotifyDataErrorInfo
in other way than regular XAML binding, documentation didn't help me in this topic, only sample application shows me how to do this in regular XAML.
platform: .net wpf
version: ReactiveUI.WPF 13.2.2, Validation 2.1.1
Describe the bug
xxxWindow win = new xxxxWindow(); -----> inherit from ReactiveValidationObject
win.ShowDialog(); ---->ValidationRule is not working
win.Show(); ------> It's ok
Describe the bug
The IValidationComponent.Text
property is nullable. As such ReactiveValidationObject.SelectInvalidPropertyValidations
can return null
Expected behavior
The presence of a null
Text
property should not throw an error.
Describe the bug
Using 2 validation rules ending with the same property name, e.g.
this.ValidationRule(
viewModel => viewModel.Origin.Name,
name => !string.IsNullOrWhiteSpace(name),
"You must specify a valid origin name");
this.ValidationRule(
viewModel => viewModel.Destination.Name,
name => !string.IsNullOrWhiteSpace(name),
"You must specify a valid destination name");
and then
this.BindValidation(ViewModel, vm => vm.Origin.Name, view => view.OriginNameError.Text);
this.BindValidation(ViewModel, vm => vm.Destination.Name, view => view.DestinationNameError.Text);
results in view.OriginNameError and view.DestinationNameError having all "*.Name" errors.
Steps To Reproduce
Use example mentioned before.
Expected behavior
Errors with the same last property name should not "share" errors.
Screenshots
Environment
but the bug is present on all platforms.
Additional context
I have fixed this bug by replacing all instances of "Body.GetMemberInfo().Name" in the code by "Body.GetPropertyPath()", which returns "Origin.Name" and "Destination.Name" instead of only the "Name".
I know you might be in the process of refactoring property validation, but if you are ready, I'll make a pull request.
GetPropertyPath code:
// <copyright file="ReactiveUI.Validation/src/ReactiveUI.Validation/Extensions/ExpressionExtensions.cs" company=".NET Foundation">
// 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 more information.
// </copyright>
using System.Linq.Expressions;
using System.Text;
namespace ReactiveUI.Validation.Extensions
{
/// <summary>
/// Extensions methods associated to <see cref="Expression"/> instances.
/// </summary>
public static class ExpressionExtensions
{
/// <summary>
/// Returns a property path expression as a string.
/// </summary>
/// <param name="expression">The property path expression.</param>
/// <returns>The property path string representing the expression.</returns>
public static string GetPropertyPath(this Expression expression)
{
var path = new StringBuilder();
while (expression is MemberExpression memberExpression)
{
if (path.Length > 0)
path.Insert(0, '.');
path.Insert(0, memberExpression.Member.Name);
expression = memberExpression.Expression;
}
return path.ToString();
}
}
}
Is your feature request related to a problem? Please describe.
Had a case where i needed to have validations that stop and block an external process and others that simply display the error without blocking or stopping anything. I also needed to display these validations separately.
Describe the solution you'd like
I thinked of having multiple validation contexts in a single ViewModel. To do so I made overloads for the "ValidationRule(...)" and "BindValidation(...)" methods. I added a "Func<TViewModel, ValidationContext> validationContextProperty" parameter to those methods and use it instead of the one present by default in the "IValidatableViewModel" interface.
Describe alternatives you've considered
An alternative could have been to create multiple validation classes inheriting from IValidationComponent but that also means adding methods to add them to the validation context. I would also need to create functions to filter them in order to know if a validation of type x is false.
Describe suggestions on how to achieve the feature
Made modifications of the code of this repo and uploaded everything on this github in the multiple validation context branch.
https://github.com/brignolff/Mdified.ReactiveUI.Vaidation/tree/multiple_validation_contexts
Might be useful to get some benchmarks using benchmark dot net
I would place them in the /benchmarks and in their own solution to make it easier in the build server.
Describe the bug
When using a complex validation rule specifying a property name, e.g.
this.ValidationRule(
m => m.TextInput2,
m => m.WhenAnyValue(m1 => m1.TextInput1, m1 => m1.TextInput2).Select(both => both.Item1 == both.Item2),
(vm, isValid) => isValid ? string.Empty : "Both inputs should be the same");
and then
this.BindValidation(ViewModel, vm => vm.TextInput2, view => view.TextInput2Error.Text);
results in no errors in view.TextInput2Error.
This happens because ValidationContext extensions ResolveFor and ResolveForMultiple have not been updated to use the IPropertyValidationComponent interface.
This line:
skips over ModelObservableValidationBase objects.
Steps To Reproduce
Use example mentioned before.
Expected behavior
Validation should bind to view.
Screenshots
Environment
but the bug is present on all platforms.
Additional context
I have fixed this bug by refactoring ValidationContext extensions to use the IPropertyValidationComponent interface. I attached the refactored extensions which use the GetPropertyPath extension mentioned in issue #60.
NOTE: There is another bug lurking in the ValidationContext extensions. The overloads with multiple property expressions have typos which reuse TProperty1 instead of TProperty2 or TProperty3:
I know you might be in the process of refactoring property validation, but if you are ready, I'll make a pull request.
ValidationContextExtensions.cs:
// <copyright file="ReactiveUI.Validation/src/ReactiveUI.Validation/Extensions/ValidationContextExtensions.cs" company=".NET Foundation">
// 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 more information.
// </copyright>
using System;
using System.Linq;
using System.Linq.Expressions;
using ReactiveUI.Validation.Components.Abstractions;
using ReactiveUI.Validation.Contexts;
using ReactiveUI.Validation.Exceptions;
namespace ReactiveUI.Validation.Extensions
{
/// <summary>
/// Extensions methods for <see cref="ValidationContext"/>.
/// </summary>
public static class ValidationContextExtensions
{
/// <summary>
/// Resolves the property valuation for a specified property.
/// </summary>
/// <typeparam name="TViewModel">ViewModel type.</typeparam>
/// <typeparam name="TViewModelProperty">ViewModel property type.</typeparam>
/// <param name="context">ValidationContext instance.</param>
/// <param name="viewModelProperty">ViewModel property.</param>
/// <param name="strict">Indicates if the ViewModel property to find is unique.</param>
/// <returns>Returns a <see cref="IPropertyValidationComponent{TViewModel}"/> object if has a single validation property,
/// otherwise will throw a <see cref="MultipleValidationNotSupportedException"/> exception.</returns>
/// <exception cref="MultipleValidationNotSupportedException">
/// Thrown if the ViewModel property has more than one validation associated.
/// </exception>
public static IPropertyValidationComponent<TViewModel> ResolveFor<TViewModel, TViewModelProperty>(
this ValidationContext context,
Expression<Func<TViewModel, TViewModelProperty>> viewModelProperty,
bool strict = true)
{
var validations = context.Validations
.OfType<IPropertyValidationComponent<TViewModel>>()
.Where(v => v.ContainsProperty(viewModelProperty, strict))
.ToList();
if (validations.Count > 1)
{
throw new MultipleValidationNotSupportedException(viewModelProperty.Body.GetPropertyPath());
}
return validations[0];
}
/// <summary>
/// Resolves the property valuation for two properties.
/// </summary>
/// <typeparam name="TViewModel">ViewModel type.</typeparam>
/// <typeparam name="TProperty1">ViewModel first property type.</typeparam>
/// <typeparam name="TProperty2">ViewModel second property type.</typeparam>
/// <param name="context">ValidationContext instance.</param>
/// <param name="viewModelProperty1">First ViewModel property.</param>
/// <param name="viewModelProperty2">Second ViewModel property.</param>
/// <returns>Returns a <see cref="IPropertyValidationComponent{TViewModel}"/> object if has a single validation property,
/// otherwise will throw a <see cref="MultipleValidationNotSupportedException"/> exception.</returns>
/// <exception cref="MultipleValidationNotSupportedException">
/// Thrown if the ViewModel property has more than one validation associated.
/// </exception>
public static IPropertyValidationComponent<TViewModel> ResolveFor<TViewModel, TProperty1,
TProperty2>(
this ValidationContext context,
Expression<Func<TViewModel, TProperty1>> viewModelProperty1,
Expression<Func<TViewModel, TProperty2>> viewModelProperty2)
{
var validations = context
.Validations
.OfType<IPropertyValidationComponent<TViewModel>>()
.Where(v =>
v.ContainsProperty(viewModelProperty1) && v.ContainsProperty(viewModelProperty2)
&& v.PropertyCount == 2)
.ToList();
if (validations.Count > 1)
{
throw new MultipleValidationNotSupportedException(
viewModelProperty1.Body.GetPropertyPath(),
viewModelProperty2.Body.GetPropertyPath());
}
return validations[0];
}
/// <summary>
/// Resolves the property valuation for three properties.
/// </summary>
/// <typeparam name="TViewModel">ViewModel type.</typeparam>
/// <typeparam name="TProperty1">ViewModel first property type.</typeparam>
/// <typeparam name="TProperty2">ViewModel second property type.</typeparam>
/// <typeparam name="TProperty3">ViewModel third property type.</typeparam>
/// <param name="context">ValidationContext instance.</param>
/// <param name="viewModelProperty1">First ViewModel property.</param>
/// <param name="viewModelProperty2">Second ViewModel property.</param>
/// <param name="viewModelProperty3">Third ViewModel property.</param>
/// <returns>Returns a <see cref="IPropertyValidationComponent{TViewModel}"/> object if has a single validation property,
/// otherwise will throw a <see cref="MultipleValidationNotSupportedException"/> exception.</returns>
/// <exception cref="MultipleValidationNotSupportedException">
/// Thrown if the ViewModel property has more than one validation associated.
/// </exception>
public static IPropertyValidationComponent<TViewModel> ResolveFor<TViewModel,
TProperty1, TProperty2, TProperty3>(
this ValidationContext context,
Expression<Func<TViewModel, TProperty1>> viewModelProperty1,
Expression<Func<TViewModel, TProperty2>> viewModelProperty2,
Expression<Func<TViewModel, TProperty3>> viewModelProperty3)
{
var validations = context
.Validations
.OfType<IPropertyValidationComponent<TViewModel>>()
.Where(v =>
v.ContainsProperty(viewModelProperty1) && v.ContainsProperty(viewModelProperty2)
&& v.ContainsProperty(viewModelProperty3)
&& v.PropertyCount == 3)
.ToList();
if (validations.Count > 1)
{
throw new MultipleValidationNotSupportedException(
viewModelProperty1.Body.GetPropertyPath(),
viewModelProperty2.Body.GetPropertyPath(),
viewModelProperty3.Body.GetPropertyPath());
}
return validations[0];
}
}
}
ValidationContextMultipleExtensions.cs:
// <copyright file="ReactiveUI.Validation/src/ReactiveUI.Validation/Extensions/ValidationContextMultipleExtensions.cs" company=".NET Foundation">
// 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 more information.
// </copyright>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using ReactiveUI.Validation.Components.Abstractions;
using ReactiveUI.Validation.Contexts;
namespace ReactiveUI.Validation.Extensions
{
/// <summary>
/// Extensions methods for <see cref="ValidationContext"/> which supports multiple validations.
/// </summary>
public static class ValidationContextMultipleExtensions
{
/// <summary>
/// Resolves all the properties valuations for a specified property.
/// </summary>
/// <typeparam name="TViewModel">ViewModel type.</typeparam>
/// <typeparam name="TViewModelProperty">ViewModel property type.</typeparam>
/// <param name="context">ValidationContext instance.</param>
/// <param name="viewModelProperty">ViewModel property.</param>
/// <param name="strict">Indicates if the ViewModel property to find is unique.</param>
/// <returns>Returns a <see cref="IPropertyValidationComponent{TViewModel}"/> object.</returns>
public static IEnumerable<IPropertyValidationComponent<TViewModel>> ResolveForMultiple<TViewModel,
TViewModelProperty>(
this ValidationContext context,
Expression<Func<TViewModel, TViewModelProperty>> viewModelProperty,
bool strict = true)
{
var validations = context.Validations
.OfType<IPropertyValidationComponent<TViewModel>>()
.Where(v => v.ContainsProperty(viewModelProperty, strict));
return validations;
}
/// <summary>
/// Resolves the property valuation for two properties.
/// </summary>
/// <typeparam name="TViewModel">ViewModel type.</typeparam>
/// <typeparam name="TProperty1">ViewModel first property type.</typeparam>
/// <typeparam name="TProperty2">ViewModel second property type.</typeparam>
/// <param name="context">ValidationContext instance.</param>
/// <param name="viewModelProperty1">First ViewModel property.</param>
/// <param name="viewModelProperty2">Second ViewModel property.</param>
/// <returns>Returns a <see cref="IPropertyValidationComponent{TViewModel}"/> object.</returns>
public static IEnumerable<IPropertyValidationComponent<TViewModel>> ResolveForMultiple<
TViewModel,
TProperty1, TProperty2>(
this ValidationContext context,
Expression<Func<TViewModel, TProperty1>> viewModelProperty1,
Expression<Func<TViewModel, TProperty2>> viewModelProperty2)
{
var validations = context
.Validations
.OfType<IPropertyValidationComponent<TViewModel>>()
.Where(v => v.ContainsProperty(viewModelProperty1) && v.ContainsProperty(viewModelProperty2)
&& v.PropertyCount == 2);
return validations;
}
/// <summary>
/// Resolves the property valuation for three properties.
/// </summary>
/// <typeparam name="TViewModel">ViewModel type.</typeparam>
/// <typeparam name="TProperty1">ViewModel first property type.</typeparam>
/// <typeparam name="TProperty2">ViewModel second property type.</typeparam>
/// <typeparam name="TProperty3">ViewModel third property type.</typeparam>
/// <param name="context">ValidationContext instance.</param>
/// <param name="viewModelProperty1">First ViewModel property.</param>
/// <param name="viewModelProperty2">Second ViewModel property.</param>
/// <param name="viewModelProperty3">Third ViewModel property.</param>
/// <returns>Returns a <see cref="IPropertyValidationComponent{TViewModel}"/> object.</returns>
public static IEnumerable<IPropertyValidationComponent<TViewModel>>
ResolveForMultiple<
TViewModel, TProperty1, TProperty2, TProperty3>(
this ValidationContext context,
Expression<Func<TViewModel, TProperty1>> viewModelProperty1,
Expression<Func<TViewModel, TProperty2>> viewModelProperty2,
Expression<Func<TViewModel, TProperty3>> viewModelProperty3)
{
var validations = context
.Validations
.OfType<IPropertyValidationComponent<TViewModel>>()
.Where(v => v.ContainsProperty(viewModelProperty1) && v.ContainsProperty(viewModelProperty2)
&& v.ContainsProperty(viewModelProperty3)
&& v.PropertyCount == 3);
return validations;
}
}
}
Describe the bug
When having multiple validation rules, I get an IndexOutOfRangeException
at Runtime. Both validation rules work when they are enabled individually.
Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index
The problem here is that the exception does not give enough information about the reason for the exception, which makes it impossible to fix the underlying problem with the injection.
Version Information
Steps To Reproduce
ViewModel
this.ValidationRule(vm => vm.EventId, id => Guid.TryParse(id, out var _), id => "error");
this.ValidationRule(vm => vm.EventRecurrenceId, id => Guid.TryParse(id, out var _), id => "error");
View (Code Behind)
this.BindValidation(ViewModel, vm => vm.EventId, v => v.EventIdErrorLabel.Text);
this.BindValidation(ViewModel, vm => vm.EventRecurrenceId, v => v.EventRecurrenceIdErrorLabel.Text);
View (XAML)
<TextBlock x:Name="EventIdErrorLabel" />
<TextBlock x:Name="EventRecurrenceIdErrorLabel" />
Expected behavior
ValidationContextExtensions.ResolveFor
provides a more helpful error message, when the resolving fails.
Environment
Stacktrace
at System.ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource)
at ReactiveUI.Validation.Extensions.ValidationContextExtensions.ResolveFor[TViewModel,TViewModelProperty](ValidationContext context, Expression`1 viewModelProperty, Boolean strict) in d:\a\1\s\src\ReactiveUI.Validation\Extensions\ValidationContextExtensions.cs:line 50
at ReactiveUI.Validation.ValidationBindings.ValidationBinding.<>c__DisplayClass2_0`4.<ForProperty>b__2(TViewModel viewModel) in d:\a\1\s\src\ReactiveUI.Validation\ValidationBindings\ValidationBinding.cs:line 69
Is your feature request related to a problem? Please describe.
At the core of ReactiveUI.Validation are IValidationComponent
s that expose IObservable<ValidationState>
. All the ValidationRule
extensions fundamentally boil down to producing a ValidationState
when necessary. However, it is not an ideal extensibility point because ValidationState
is a sealed class, which includes a back-reference to the 'owner' Component.
The ability to produce an ValidationState
s directly would be of great benefit. Even better would be for the API to accept an interface IValidationState
which does not require the back reference to the owner IValidationComponent
. This would allow consumption of observables that implement IValidationState
, maximising efficiency, as compared to using one of the existing ValidationRule
extension methods to manufacture the ValidationState
class for each change in validation (see for example the new method added in #116, which requires two functions to extract the validity and text from a TState).
Describe the solution you'd like
I suggest that a new IValidationState
interface is created:
public interface IValidationState
{
bool IsValid { get; }
ValidationText Text { get; }
}
This interface should be used wherever ValidationState
is currently used in the library.
ValidationState
should implement IValidationState
, but the Component
property should be removed. This would then be a utility struct for consumers to easily provide a default implementation of IValidationState
. There is also a strong case for changing ValidationState
into a struct - it being a single pointer and a bool, and having a sensible default state (though care should be taken to handle a null
Text); alternatively, the class should not be sealed, to allow it to be used as a base class for implementors.
A new ValidationRule extension should be added that that accepts an IObservable<IValidationState>
.
Describe alternatives you've considered
This is not a required feature, so no alternatives are required, however, it should be considered that accepting an interface will allow for consumers to supply simple Observables of any struct/class that implements that interface, and not use one of the more complex overloads.
Further, the Compoment
property appearing in the child element is code smell as there is a forward and back reference between Component
and ValidationState
, this is generally considered bad practice as it allows for data to become inconsistent, which can introduce bugs.
Describe suggestions on how to achieve the feature
A review of ValidationState.Component
shows that the only meaningful usage is in ReactiveValidationObject.OnValidationStatusChange, a private method call, called from an observable in the constructor:
This can be rewritten to pass the IValidationCompoment
to the method so that it does not need to get it from the IValidationState
.
Additional context
Example usage:
// IObservable<ValidationState> where ValidationState implements IValidationState
var passwordsObservable =
this.WhenAnyValue(
x => x.Password,
x => x.ConfirmPassword,
(password, confirmation) => new ValidationState(
state.Password == state.Confirmation,
$"Passwords must match: {state.Password} != {state.Confirmation}"));
this.ValidationRule(
vm => vm.ConfirmPassword,
passwordsObservable);
As noted, this is an improvement over the existing example, in that it only produces one ValidationState object, and does not need to make two function calls on each change. It is also very extensible as any object that implements IValidationState
can be consumed.
Is your feature request related to a problem? Please describe.
Currently, we support binding validations to a TextInputLayout
, but there is no sample app that shows how to use this feature.
Describe the solution you'd like
Ideally, we should add a sample app targeting Xamarin.Android (native) to our LoginApp
sample solution. Before doing that, we should rename and move the existing LoginApp.Android
and LoginApp.iOS
projects to LoginApp.Forms.Android
and LoginApp.Forms.iOS
respectively as they represent the entry points for the Xamarin.Forms app LoginApp.Forms
.
Describe suggestions on how to achieve the feature
Someone experienced in Android should create a new app with axml
UI, use WireUpControls
, and BindValidation
overload that accepts a TextInputLayout
. Ideally, we should target AndroidX #134 Also we shouldn't forget to create a separate LoginApp.Android
solution file for the new app.
Additional context
https://github.com/reactiveui/ReactiveUI.Validation#example-with-android-extensions
Is your feature request related to a problem? Please describe.
Currently, there are over 200 warning in the solution.
Describe the solution you'd like
Let's clean all those warnings.
I'm trying to do something very simple. I want to validate that the Sources ObservableCollection is not empty.
var sourceNotEmpty = Sources
.ToObservableChangeSet(x => x.Path)
.ToCollection()
.Select(x => x.Any());
this.ValidationRule(x => x.Sources,
sourceNotEmpty,
"You must select source files or folders to encode");
Testing GetErrors works except for this test. It fails because the ValidationRule hasn't yet been evaluated. If I add an item and then remove it, then it works.
[Fact]
public void GetErrors_SourceEmpty_Error()
{
var errors = Model.GetErrors(nameof(Model.Sources));
Assert.Single(errors); // errors is empty
}
Then, how do I get IsValid? Before doing some work in the ViewModel, I first need to check "IsValid" but haven't yet found the way to get the current value. LastAsync
won't work if it hasn't yet been evaluated.
[Fact]
public void IsValid_MissingData_ReturnsFalse()
{
bool? result = null;
Model.IsValid().Do(x => result = x);
Assert.False(result);
}
These 2 things are extremely simple but there's probably something I'm missing with the whole paradigm?
Adding to this, on the UI, I probably don't want to display all the "field missing" errors right away, but only when he tries to click Submit or re-empties a field after typing in it.
Describe the bug
On a clean checkout, get the following errors on build.
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\Components\BasePropertyValidation.cs(28,123,28,163): warning CS0618: 'IPropertyValidationComponent<TViewModel>' is obsolete: 'Consider using the non-generic version of an IPropertyValidationComponent.'
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\Components\ObservableValidation.cs(240,133,240,173): warning CS0618: 'IPropertyValidationComponent<TViewModel>' is obsolete: 'Consider using the non-generic version of an IPropertyValidationComponent.'
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\Components\BasePropertyValidation.cs(283,34,283,118): warning CS0618: 'ValidationState.ValidationState(bool, ValidationText, IValidationComponent)' is obsolete: 'This constructor overload is going to be removed soon.'
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\TemplateGenerators\PropertyValidationGenerator.cs(131,24,131,86): warning CS0618: 'ValidationState.ValidationState(bool, ValidationText, IValidationComponent)' is obsolete: 'This constructor overload is going to be removed soon.'
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\TemplateGenerators\PropertyValidationGenerator.cs(277,24,277,86): warning CS0618: 'ValidationState.ValidationState(bool, ValidationText, IValidationComponent)' is obsolete: 'This constructor overload is going to be removed soon.'
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\TemplateGenerators\PropertyValidationGenerator.cs(769,24,769,86): warning CS0618: 'ValidationState.ValidationState(bool, ValidationText, IValidationComponent)' is obsolete: 'This constructor overload is going to be removed soon.'
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\Helpers\ReactiveValidationObject.cs(93,17,94,32): error CS0012: The type 'IAsyncOperation<>' is defined in an assembly that is not referenced. You must add a reference to assembly 'Windows.Foundation.FoundationContract, Version=3.0.0.0, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime'.
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\Helpers\ReactiveValidationObject.cs(93,17,94,32): error CS0012: The type 'IAsyncOperationWithProgress<,>' is defined in an assembly that is not referenced. You must add a reference to assembly 'Windows.Foundation.FoundationContract, Version=3.0.0.0, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime'.
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\Helpers\ReactiveValidationObject.cs(96,17,98,32): error CS0012: The type 'IAsyncOperation<>' is defined in an assembly that is not referenced. You must add a reference to assembly 'Windows.Foundation.FoundationContract, Version=3.0.0.0, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime'.
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\Helpers\ReactiveValidationObject.cs(96,17,98,32): error CS0012: The type 'IAsyncOperationWithProgress<,>' is defined in an assembly that is not referenced. You must add a reference to assembly 'Windows.Foundation.FoundationContract, Version=3.0.0.0, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime'.
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\TemplateGenerators\PropertyValidationGenerator.cs(432,24,432,86): warning CS0618: 'ValidationState.ValidationState(bool, ValidationText, IValidationComponent)' is obsolete: 'This constructor overload is going to be removed soon.'
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\TemplateGenerators\PropertyValidationGenerator.cs(596,24,596,86): warning CS0618: 'ValidationState.ValidationState(bool, ValidationText, IValidationComponent)' is obsolete: 'This constructor overload is going to be removed soon.'
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\Contexts\ValidationContext.cs(56,13,58,27): error CS0012: The type 'CoreDispatcher' is defined in an assembly that is not referenced. You must add a reference to assembly 'Windows.Foundation.UniversalApiContract, Version=7.0.0.0, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime'.
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\Contexts\ValidationContext.cs(56,13,58,27): error CS0012: The type 'DependencyObject' is defined in an assembly that is not referenced. You must add a reference to assembly 'Windows.Foundation.UniversalApiContract, Version=7.0.0.0, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime'.
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\Contexts\ValidationContext.cs(89,30,89,77): warning CS0618: 'ValidationState.ValidationState(bool, ValidationText, IValidationComponent)' is obsolete: 'This constructor overload is going to be removed soon.'
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\ValidationBindings\ValidationBinding.cs(74,25,77,28): error CS0012: The type 'IAsyncOperation<>' is defined in an assembly that is not referenced. You must add a reference to assembly 'Windows.Foundation.FoundationContract, Version=3.0.0.0, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime'.
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\ValidationBindings\ValidationBinding.cs(74,25,77,28): error CS0012: The type 'IAsyncOperationWithProgress<,>' is defined in an assembly that is not referenced. You must add a reference to assembly 'Windows.Foundation.FoundationContract, Version=3.0.0.0, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime'.
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\ValidationBindings\ValidationBinding.cs(131,25,134,28): error CS0012: The type 'IAsyncOperation<>' is defined in an assembly that is not referenced. You must add a reference to assembly 'Windows.Foundation.FoundationContract, Version=3.0.0.0, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime'.
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\ValidationBindings\ValidationBinding.cs(131,25,134,28): error CS0012: The type 'IAsyncOperationWithProgress<,>' is defined in an assembly that is not referenced. You must add a reference to assembly 'Windows.Foundation.FoundationContract, Version=3.0.0.0, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime'.
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\ValidationBindings\ValidationBinding.cs(188,34,190,36): error CS0012: The type 'IAsyncOperationWithProgress<,>' is defined in an assembly that is not referenced. You must add a reference to assembly 'Windows.Foundation.FoundationContract, Version=3.0.0.0, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime'.
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\ValidationBindings\ValidationBinding.cs(245,34,247,36): error CS0012: The type 'IAsyncOperationWithProgress<,>' is defined in an assembly that is not referenced. You must add a reference to assembly 'Windows.Foundation.FoundationContract, Version=3.0.0.0, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime'.
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\ValidationBindings\ValidationBinding.cs(291,25,294,28): error CS0012: The type 'IAsyncOperation<>' is defined in an assembly that is not referenced. You must add a reference to assembly 'Windows.Foundation.FoundationContract, Version=3.0.0.0, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime'.
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\ValidationBindings\ValidationBinding.cs(291,25,294,28): error CS0012: The type 'IAsyncOperationWithProgress<,>' is defined in an assembly that is not referenced. You must add a reference to assembly 'Windows.Foundation.FoundationContract, Version=3.0.0.0, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime'.
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\ValidationBindings\ValidationBinding.cs(335,25,338,28): error CS0012: The type 'IAsyncOperation<>' is defined in an assembly that is not referenced. You must add a reference to assembly 'Windows.Foundation.FoundationContract, Version=3.0.0.0, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime'.
1>C:\Source\ReactiveUI.Validation\src\ReactiveUI.Validation\ValidationBindings\ValidationBinding.cs(335,25,338,28): error CS0012: The type 'IAsyncOperationWithProgress<,>' is defined in an assembly that is not referenced. You must add a reference to assembly 'Windows.Foundation.FoundationContract, Version=3.0.0.0, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime'.
1>Done building project "ReactiveUI.Validation.csproj" -- FAILED.
They all relate to IAsyncOperation
and IAsyncOperationWithProgress
being missing on the target.
These disappeared when removing the uap10.0.17763
target from the project file.
Steps To Reproduce
Clean check out, build.
Expected behavior
Build without errors.
Environment
(Following a discussion on Slack)
Currently, to utilise 3rd library controls that do not expose their error display elements, you have to use one of the XAML binding syntaxes such as:
Text="{Binding Path=Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True"
Or do this in code-behind by either creating the binding, or retrieve the binding using BindingOperations.GetBinding(DependencyObject, DependencyProperty)
and then accessing the ValidatesOnNotifyDataErrors
, ValidatesOnDataErrors
, ValidatesOnExceptions
properties directly.
However, the ReactiveUI Bind system introduces IReactiveBinding<...>
which does not expose these properties, nor can you retrieve a binding from the control as creating an IReactiveBinding<...>
does not create a System.Windows.Data.Binding
. As such you are required to Bind the control using one of the original non-reactive binding methods.
I propose expanding IReactiveBinding<...>
to include these properties to better support validation scenarios. At the same time, we should review whether to expose ValidationRules
on the binding (as per the original).
platform: .net wpf
version: ReactiveUI.WPF 13.2.2, Validation 2.1.1
Describe the bug
xxxWindow win = new xxxxWindow(); -----> inherit from ReactiveValidationObject
win.ShowDialog(); ---->ValidationRule is not working
win.Show(); ------> It's ok
The following classes exist:
public class ParentEntity: ReactiveValidationObject
{
public string ParentName {get; set;}
public ObservableCollection<ChildEntity> Items { get; }
}
public class ChildEntity: ReactiveValidationObject
{
public string Name {get; set;}
}
How can I write a ValidationRule for ParentEntity that also automatically checks the Collection "Items"?
Is your feature request related to a problem? Please describe.
Not related to a problem.
Describe the solution you'd like
So what I'm trying to do is change the color of control in my view based on the validation state of a validation helper. The control does have an error property or something similar, and I do not want to show an inline error message anyway for this particular situation. What I would like to be able to do is something like this:
this.BindValidation(ViewModel, viewModel => viewModel.Property, view => view.Control.Color,
validationState => validationState.IsValid ? Color.Green : Color.Red);
This would similar to the overload of the OneWayBind function that allows you to bind between types like so:
this.OneWayBind(ViewModel, viewModel => vm.BoolProperty, view => view.Control.Color,
boolProperty => boolProperty ? Color.Green : Color.Red);
Describe alternatives you've considered
Currently this is how I'm achieving the same effect:
this.WhenAnyObservable(v => v.ViewModel.PropertyValidator.ValidationChanged)
.Select(validationState => validationState.IsValid ? Color.Green : Color.Red)
.BindTo(this, v => v.Control.Color);
This works, and I don't necessarily hate it, but I would prefer the proposed form for brevity and to match with the rest of my simple bindings.
Describe suggestions on how to achieve the feature
I'm not sure exactly how to make this work or I probably just would have made done it and made PR, but I think the signatures of the overloads for such a change would look something like this:
public static IDisposable BindValidation<TView, TViewModel, TViewModelProperty, TViewProperty>(
this TView view,
TViewModel? viewModel,
Expression<Func<TViewModel, TViewModelProperty>> viewModelProperty,
Expression<Func<TView, TViewProperty>> viewProperty,
Expression<Func<IValidationState, TViewProperty>> selector,
IValidationTextFormatter<string>? formatter = null)
where TView : IViewFor<TViewModel>
where TViewModel : class, IReactiveObject, IValidatableViewModel
and this:
public static IDisposable BindValidation<TView, TViewModel, TViewProperty>(
this TView view,
TViewModel? viewModel,
Expression<Func<TViewModel?, ValidationHelper?>> viewModelHelperProperty,
Expression<Func<TView, TViewProperty>> viewProperty,
Expression<Func<IValidationState, TViewProperty>> selector,
IValidationTextFormatter<string>? formatter = null)
where TView : IViewFor<TViewModel>
where TViewModel : class, IReactiveObject, IValidatableViewModel
Additional context
Is your feature request related to a problem? Please describe.
There are no samples for WPF, Windows Forms, and UWP now. Let's fix that.
Describe the solution you'd like
We should create samples for WPF and WF.
Describe suggestions on how to achieve the feature
Probably we should port the existing XF sample to WPF and WF.
Worth sharing existing view models across many platforms.
Update
Now we have samples for WPF and Avalonia in the samples directory. Still missing the samples for Windows Forms and UWP.
Is your feature request related to a problem? Please describe.
Actually, if you try to add two validations to the same property, eg Password
, in the ValidationContext you would do it like this:
[โฆ]
this.ValidationRule(
vm => vm.Password,
password => !string.IsNullOrEmpty(password),
"Password is required.");
this.ValidationRule(
vm => vm.Password,
password => password.Length > 5,
"Password minimum length is 5.");
[โฆ]
And from the View you would bind to the Password
property like this:
[โฆ]
this.BindValidation(ViewModel, vm => vm.Password, view => view.PasswordErrorMessage.Text)
.DisposeWith(disposables);
[โฆ]
The problem by doing this is that from the ValidationBindings
, used by the ViewForExtensions
, it looks up only for the first or default validation rule (see ResolveFor
methods in ValidationContextExtensions
).
Describe the solution you'd like
Add support for multiple validations in the same property since it's supported in the ValidationContext side.
Describe suggestions on how to achieve the feature
Basically, if more than one property matches the query in the ResolveFor
method then unless the Formatter has been provided, it should throw a custom exception like MultipleValidationNotSupportedException
.
On the other side, we should provide Mixins to give support for multiple validation scenarios.
Describe the bug
UIKit.UIKitThreadAccessException in iOS as canExecute: this.IsValid() doesn't tick in MainThread
Steps To Reproduce
Bind Command to any Xamarin Forms Button,
SomeCommand = ReactiveCommand.CreateFromObservable(
observable,
canExecute: this.IsValid());
UIKit.UIKitThreadAccessException is thrown
Expected behavior
No thrown UIKit.UIKitThreadAccessException
Environment
Additional context
Workaround:
SomeButtonCommand = ReactiveCommand.CreateFromObservable(
observable,
canExecute: this.IsValid().ObserveOn(RxApp.MainThreadScheduler));
Is your feature request related to a problem? Please describe.
INotifyDataErrorInfo isn't currently supported by ReactiveUI and ReactiveUI.Validation, although one could write a helper class as shown below in the "Additional Context" section.
INotifyDataErrorInfo
is supported by WPF, by Xamarin.Forms and also by AvaloniaUI, so worth providing reactive validation helpers (e.g. use IObservable
s instead of events) compatible with INotifyDataErrorInfo
as well. Worth noting, that such validation mechanics aren't supported by e.g. UWP so we should restrict the usage of such helpers on unsupported platforms.
Describe the solution you'd like
Probably we could create a base class for view models that support validation, as follows:
public abstract class ReactiveValidationObject
: ReactiveObject,
ISupportsValidation,
INotifyDataErrorInfo {
// implementation details
}
But open to any other suggestions. Also worth looking through ReactiveValidation library concepts.
Additional context
Proposed INotifyDataErrorInfo
usage with AvaloniaUI:
public class MainWindowViewModel : ReactiveValidationObject<MainWindowViewModel>
{
[Reactive]
public string Message { get; set; }
public MainWindowViewModel()
{
this.ValidationRule(
x => x.Message,
x => x.Length > 0,
"Message shouldn't be empty.");
}
}
AvaloniaUI markup:
<StackPanel Margin="10">
<TextBlock FontSize="16" Text="INotifyDataErrorInfo Validation"/>
<TextBox Watermark="Message" Text="{Binding Message}"/>
</StackPanel>
Beware, this works only with string
properties, to support all other types and to remove generic type arguments we need to change ReactiveUI.Validation
code.
[Reactive]
public double MaxValue { get; set; }
[Reactive]
public double MinValue { get; set; }
If I were to have the above code (code weaved to be a notify property at build time) and I were to write:
var minMaxObs = this.WhenAnyValue(v => v.MinValue, v => v.MaxValue,
(min, max) => min < max)
this.ValidationRule(v => v.MaxValue, minMaxObs,
"Value must be less than max value.");
this.WhenAnyPropertyChanged("MaxValue").Subscribe(_ => {
// save to database here
});
Then when inside my subscribe callback HasErrors
is false when a validation error has occurred. It seems like the state of HasErrors
is behind one, because if the property is updated again and still invalid, HasErrors
will be true.
I've tried re-ordering my observables so that validation rules were last, that didn't work.
EDIT:
This seems to be caused by WhenAnyPropertyChanged
being called before WhenAnyValue
, even though the order that subscribers are set up are right. This may be an upstream issue. A workaround I found is dispatching on the UI thread the subscription of the 'WhenAnyPropertyChanged' callback. I'm using WhenAnyPropertyChanged
because I have a framework I created where properties marked with a specified attribute automatically save changes to the database, without requiring me to make observables for all of them.
Create a notifiable property and subscribe to changes via ReactiveUI WhenAnyPropertyChanged
extension. Notice that validation status is wrong within subscription callback.
https://github.com/reactiveui/ReactiveUI
HasErrors
should correctly reflect validation status.
No response
No response
Windows
11
PC
3.1.7
No response
Match RxUI
And remove warnings/errors from the project.
Is your feature request related to a problem? Please describe.
Xamarin.Forms will ship a new major version later this year, and continue to ship minor and service releases every 6 weeks through .NET 6 GA in November 2021. The final release of Xamarin.Forms will be serviced for a year after shipping, and all modern work will shift to .NET MAUI. โ https://devblogs.microsoft.com/dotnet/introducing-net-multi-platform-app-ui/
Since Xamarin.Forms is going to be replaced by MAUI, probably worth creating a sample app that demonstrates how to use MAUI with ReactiveUI.Validation. Preferably the new app targeting MAUI should live inside https://github.com/reactiveui/ReactiveUI.Validation/tree/main/samples and reuse the view models from LoginApp
as Avalonia, UWP, WPF and Windows Forms sample apps do.
Describe the bug
I'm attempting to use BindValidationEx to bind to a specific property's validation in a Desktop WPF application
It appears that the issue described in #19 is still occurring for BindValidationEx if ViewModel is assigned and activated after view construction. Also, even by forcing it to work by passing ImmediateScheduler.Instance to the ValidationContext constructor it introduces an exception as soon as validation changes.
Steps To Reproduce
Provide the steps to reproduce the behavior:
System.Reflection.TargetInvocationException
HResult=0x80131604
Message=Exception has been thrown by the target of an invocation.
Source=mscorlib
StackTrace:
at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)
at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)
at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at System.Reflection.RuntimePropertyInfo.SetValue(Object obj, Object value, BindingFlags invokeAttr, Binder binder, Object[] index, CultureInfo culture)
at System.Reflection.RuntimePropertyInfo.SetValue(Object obj, Object value, Object[] index)
at ReactiveUI.Reflection.TrySetValueToPropertyChain[TValue](Object target, IEnumerable1 expressionChain, TValue value, Boolean shouldThrow) in d:\a\1\s\src\ReactiveUI\Expression\Reflection.cs:line 324 at ReactiveUI.PropertyBinderImplementation.<>c__DisplayClass12_0
5.b__7(Tuple2 isVmWithLatestValue) in d:\a\1\s\src\ReactiveUI\Bindings\Property\PropertyBinderImplementation.cs:line 596 at System.Reactive.AnonymousSafeObserver
1.OnNext(T value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\AnonymousSafeObserver.cs:line 43
at System.Reactive.Sink1.ForwardOnNext(TTarget value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Sink.cs:line 49 at System.Reactive.IdentitySink
1.OnNext(T value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\IdentitySink.cs:line 15
at System.Reactive.Subjects.Subject1.OnNext(T value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Subjects\Subject.cs:line 150 at System.Reactive.Sink
1.ForwardOnNext(TTarget value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Sink.cs:line 49
at System.Reactive.IdentitySink1.OnNext(T value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\IdentitySink.cs:line 15 at System.Reactive.Sink
1.ForwardOnNext(TTarget value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Sink.cs:line 49
at System.Reactive.Linq.ObservableImpl.Where1.Predicate._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Where.cs:line 54 at System.Reactive.Sink
1.ForwardOnNext(TTarget value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Sink.cs:line 49
at System.Reactive.Linq.ObservableImpl.Select2.Selector._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Select.cs:line 48 at System.Reactive.Linq.ObservableImpl.Merge
1.Observables._.InnerObserver.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Merge.cs:line 238
at System.Reactive.Linq.ObservableImpl.Select2.Selector._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Select.cs:line 48 at System.Reactive.Sink
1.ForwardOnNext(TTarget value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Sink.cs:line 49
at System.Reactive.Linq.ObservableImpl.Select2.Selector._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Select.cs:line 47 at System.Reactive.Sink
1.ForwardOnNext(TTarget value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Sink.cs:line 49
at System.Reactive.Linq.ObservableImpl.DistinctUntilChanged2._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\DistinctUntilChanged.cs:line 72 at System.Reactive.Sink
1.ForwardOnNext(TTarget value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Sink.cs:line 49
at System.Reactive.Linq.ObservableImpl.Select2.Selector._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Select.cs:line 47 at System.Reactive.Sink
1.ForwardOnNext(TTarget value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Sink.cs:line 49
at System.Reactive.Linq.ObservableImpl.Where1.Predicate._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Where.cs:line 54 at System.Reactive.Sink
1.ForwardOnNext(TTarget value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Sink.cs:line 49
at System.Reactive.Linq.ObservableImpl.Switch1._.InnerObserver.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Switch.cs:line 100 at System.Reactive.Sink
1.ForwardOnNext(TTarget value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Sink.cs:line 49
at System.Reactive.IdentitySink1.OnNext(T value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\IdentitySink.cs:line 15 at System.Reactive.Sink
1.ForwardOnNext(TTarget value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Sink.cs:line 49
at System.Reactive.Linq.ObservableImpl.Select2.Selector._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Select.cs:line 47 at System.Reactive.AutoDetachObserver
1.OnNextCore(T value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\AutoDetachObserver.cs:line 61
at System.Reactive.ObserverBase`1.OnNext(T value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\ObserverBase.cs:line 34
at ReactiveUI.DependencyObjectObservableForProperty.<>c__DisplayClass1_1.b__1(Object o, EventArgs e) in d:\a\1\s\src\ReactiveUI.WPF\DependencyObjectObservableForProperty.cs:line 56
at MS.Internal.ComponentModel.PropertyChangeTracker.OnPropertyInvalidation(DependencyObject d, DependencyPropertyChangedEventArgs args)
at System.Windows.DependentList.InvalidateDependents(DependencyObject source, DependencyPropertyChangedEventArgs sourceArgs)
at System.Windows.DependencyObject.NotifyPropertyChange(DependencyPropertyChangedEventArgs args)
at System.Windows.DependencyObject.UpdateEffectiveValue(EntryIndex entryIndex, DependencyProperty dp, PropertyMetadata metadata, EffectiveValueEntry oldEntry, EffectiveValueEntry& newEntry, Boolean coerceWithDeferredReference, Boolean coerceWithCurrentValue, OperationType operationType)
at System.Windows.DependencyObject.SetValueCommon(DependencyProperty dp, Object value, PropertyMetadata metadata, Boolean coerceWithDeferredReference, Boolean coerceWithCurrentValue, OperationType operationType, Boolean isInternal)
at System.Windows.DependencyObject.SetCurrentDeferredValue(DependencyProperty dp, DeferredReference deferredReference)
at System.Windows.Controls.TextBox.OnTextContainerChanged(Object sender, TextContainerChangedEventArgs e)
at System.Windows.Documents.TextContainerChangedEventHandler.Invoke(Object sender, TextContainerChangedEventArgs e)
at System.Windows.Documents.TextContainer.EndChange(Boolean skipEvents)
at System.Windows.Documents.TextContainer.System.Windows.Documents.ITextContainer.EndChange(Boolean skipEvents)
at System.Windows.Documents.TextRangeBase.EndChange(ITextRange thisRange, Boolean disableScroll, Boolean skipEvents)
at System.Windows.Documents.TextRange.System.Windows.Documents.ITextRange.EndChange(Boolean disableScroll, Boolean skipEvents)
at System.Windows.Documents.TextRange.ChangeBlock.System.IDisposable.Dispose()
at System.Windows.Documents.TextEditorTyping.DoTextInput(TextEditor This, String textData, Boolean isInsertKeyToggled, Boolean acceptControlCharacters)
at System.Windows.Documents.TextEditorTyping.TextInputItem.Do()
at System.Windows.Documents.TextEditorTyping.ScheduleInput(TextEditor This, InputItem item)
at System.Windows.Documents.TextEditorTyping.OnTextInput(Object sender, TextCompositionEventArgs e)
at System.Windows.Controls.Primitives.TextBoxBase.OnTextInput(TextCompositionEventArgs e)
at System.Windows.UIElement.OnTextInputThunk(Object sender, TextCompositionEventArgs e)
at System.Windows.Input.TextCompositionEventArgs.InvokeEventHandler(Delegate genericHandler, Object genericTarget)
at System.Windows.RoutedEventArgs.InvokeHandler(Delegate handler, Object target)
at System.Windows.RoutedEventHandlerInfo.InvokeHandler(Object target, RoutedEventArgs routedEventArgs)
at System.Windows.EventRoute.InvokeHandlersImpl(Object source, RoutedEventArgs args, Boolean reRaised)
at System.Windows.UIElement.RaiseEventImpl(DependencyObject sender, RoutedEventArgs args)
at System.Windows.UIElement.RaiseTrustedEvent(RoutedEventArgs args)
at System.Windows.UIElement.RaiseEvent(RoutedEventArgs args, Boolean trusted)
at System.Windows.Input.InputManager.ProcessStagingArea()
at System.Windows.Input.InputManager.ProcessInput(InputEventArgs input)
at System.Windows.Input.TextCompositionManager.UnsafeCompleteComposition(TextComposition composition)
at System.Windows.Input.TextCompositionManager.PostProcessInput(Object sender, ProcessInputEventArgs e)
at System.Windows.Input.InputManager.RaiseProcessInputEventHandlers(ProcessInputEventHandler postProcessInput, ProcessInputEventArgs processInputEventArgs)
at System.Windows.Input.InputManager.ProcessStagingArea()
at System.Windows.Input.InputManager.ProcessInput(InputEventArgs input)
at System.Windows.Input.TextCompositionManager.UnsafeStartComposition(TextComposition composition)
at System.Windows.Input.TextCompositionManager.PostProcessInput(Object sender, ProcessInputEventArgs e)
at System.Windows.Input.InputManager.RaiseProcessInputEventHandlers(ProcessInputEventHandler postProcessInput, ProcessInputEventArgs processInputEventArgs)
at System.Windows.Input.InputManager.ProcessStagingArea()
at System.Windows.Input.InputManager.ProcessInput(InputEventArgs input)
at System.Windows.Input.InputProviderSite.ReportInput(InputReport inputReport)
at System.Windows.Interop.HwndKeyboardInputProvider.ProcessTextInputAction(IntPtr hwnd, WindowMessage msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
at System.Windows.Interop.HwndSource.OnPreprocessMessage(Object param)
at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
at System.Windows.Threading.Dispatcher.LegacyInvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
at System.Windows.Threading.Dispatcher.Invoke(DispatcherPriority priority, Delegate method, Object arg)
at System.Windows.Interop.HwndSource.OnPreprocessMessageThunk(MSG& msg, Boolean& handled)
at System.Windows.Interop.HwndSource.WeakEventPreprocessMessage.OnPreprocessMessage(MSG& msg, Boolean& handled)
at System.Windows.Interop.ThreadMessageEventHandler.Invoke(MSG& msg, Boolean& handled)
at System.Windows.Interop.ComponentDispatcherThread.RaiseThreadMessage(MSG& msg)
at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
at System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
at System.Windows.Application.RunDispatcher(Object ignore)
at System.Windows.Application.RunInternal(Window window)
at System.Windows.Application.Run(Window window)
at System.Windows.Application.Run()
at TestViews.App.Main()
Inner Exception 1:
InvalidOperationException: Sequence contains no matching element
Environment
Additional context
BindValidationEx.zip
Describe the bug
ValidationContext does produce updates and current values do not reflect correct validation status or update in response to property changes.
Tested in a UWP project.
Steps To Reproduce
Create a Simple ViewModel and View with associated Bindings.
Create simple validation rules for the ViewModel
Create subscriptions to various Validation public APIs (ValidationContext.ValidationStatusChange, ValidationHelper.ValidationChange, etc.)
Run project and update text in View Controls
Observe that the behavior isn't as expected.
Expected behavior
I'd expect the ValidationContext to produce Observable sequeneces indicating the current status of the ViewModel's validation.
Environment
Additional context
I'm seeing updates to ValidationHelper.ValidationChanged in accordance with the associated Validation Rule, but no updates beyond the initial values from ValidationContext.
I've attached a bare bones project I've been using to identify this.
ReactiveUI.Validation.Sample.Uwp.zip
This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.
These updates await pending status checks. To force their creation now, click the checkbox below.
xunit
, xunit.runner.console
, xunit.runner.visualstudio
)These updates have all been created already. Click a checkbox below to force a retry/rebase of any.
.github/workflows/build-samples.yml
.github/workflows/ci-build.yml
.github/workflows/lock.yml
dessant/lock-threads v5
.github/workflows/release.yml
samples/Directory.build.props
Roslynator.Analyzers 4.12.0
stylecop.analyzers 1.2.0-beta.556
samples/LoginApp.Avalonia/LoginApp.Avalonia.csproj
MessageBox.Avalonia 3.1.5.1
samples/LoginApp.Uwp/LoginApp.Uwp.csproj
ReactiveUI.Uwp 18.4.1
Microsoft.NETCore.UniversalWindowsPlatform 6.2.14
samples/LoginApp.WinForms/LoginApp.WinForms.csproj
samples/LoginApp.Wpf/LoginApp.Wpf.csproj
MahApps.Metro 2.4.10
samples/LoginApp/LoginApp.csproj
src/Directory.build.props
Roslynator.Analyzers 4.12.0
stylecop.analyzers 1.2.0-beta.556
Nerdbank.GitVersioning 3.6.133
Microsoft.SourceLink.GitHub 8.0.0
src/ReactiveUI.Validation.AndroidSupport/ReactiveUI.Validation.AndroidSupport.csproj
src/ReactiveUI.Validation.AndroidX/ReactiveUI.Validation.AndroidX.csproj
src/ReactiveUI.Validation.Tests/ReactiveUI.Validation.Tests.csproj
DiffEngine 15.3.0
coverlet.msbuild 6.0.2
xunit.runner.visualstudio 2.5.8
Verify.Xunit 23.4.0
PublicApiGenerator 11.1.0
Microsoft.Reactive.Testing 6.0.0
Xunit.StaFact 1.1.11
xunit.runner.console 2.7.1
xunit 2.7.1
Microsoft.NET.Test.Sdk 17.9.0
src/ReactiveUI.Validation/ReactiveUI.Validation.csproj
ReactiveUI 19.5.1
ReactiveUI 19.5.41
src/global.json
dotnet-sdk 8.0.10
MSBuild.Sdk.Extras 3.0.44
Instantiating ReactiveValidationObject with latest DynamicData package crashes the app.
start app from repo
https://github.com/tomasfil/ReactiveUI.Validation.DD.Crash
This should happen...
No response
No response
No response
No response
No response
No response
No response
Describe the bug
I'm working in a WinForms project, having the ViewModel project as an F# project, and I'm try to use ReactiveUI.Validations as saw in your login sample, but when I try to bind the same property on View with BindValidation, the library throws an expection because cannot find any validation by index. Then, after debugging step by step, even when the private field _validationSource have items in it, it doesn't propage to the Validations property and always is empty.
Special Characteristics:
Steps To Reproduce
Expected behavior
Screenshots
Environment
Additional context
Repository Sample: https://github.com/Micha-kun/ReactiveUI.Validation.BugTest
PD: I'll fill later with more data, sorry
Describe the bug
When using this.IsValid() as the argument for CanExecute in a ReactiveCOmmand.Create(), the IsValid is one user input "late".
Steps To Reproduce
This is a simple ViewModel.cs to reproduce the problem, for testing I bind it to a simple WPF TextBox:
public class ViewModel : ReactiveValidationObject<ViewModel>
{
private readonly ReactiveCommand<string, Unit> Command;
private string _myProperty;
public string MyProperty
{
get => _myProperty;
set => this.RaiseAndSetIfChanged(ref _myProperty, value);
}
public ViewModel()
{
Command = ReactiveCommand.Create<string>(x => SetMyProperty(x), this.IsValid());
this
.WhenAnyValue(x => x.MyProperty)
.Do(x => Console.WriteLine("Property value = " + x))
.InvokeCommand(Command);
this.ValidationRule(
viewModel => viewModel.MyProperty,
x => !string.IsNullOrEmpty(x) && x.Length > 3,
"Enter a value");
this.IsValid()
.Subscribe(x => Console.WriteLine("IsValid = " + x));
}
private Unit SetMyProperty(string value)
{
Console.WriteLine("Entered method");
return Unit.Default;
}
}
This is the Console output: (notice when the command is executed)
Property value = 1
Property value = 12
Property value = 123
Property value = 1234
IsValid = True
Property value = 12345
Entered method
Property value = 1234
Entered method
Property value = 123
Entered method
IsValid = False
Property value = 12
Property value = 1
Expected behavior
This is the console output I would expect:
Property value = 1
Property value = 12
Property value = 123
Property value = 1234
IsValid = True
Entered method
Property value = 12345
Entered method
Property value = 1234
Entered method
Property value = 123
IsValid = False
Property value = 12
Property value = 1
Environment
Thank you so much for your help!
Describe the issue
It is currently not easily possible to overwrite the ValidationContext
class for unit testing. It would be better to make it sealed
and implement a base interface to allow for inversion of control (IOC) in unit tests.
Instead of
public interface ISupportsValidation
{
/// <summary>Gets get the validation context.</summary>
ValidationContext ValidationContext { get; }
}
public class ValidationContext : ReactiveObject, IDisposable, IValidationComponent
{
// ...
}
use
public interface IValidationContext : IDisposable, IValidationComponent
{
// ....
}
public sealed class ValidationContext : ReactiveObject, IValidationContext
{
// ...
}
public interface ISupportsValidation
{
/// <summary>Gets get the validation context.</summary>
IValidationContext ValidationContext { get; }
}
Is your feature request related to a problem? Please describe.
The ValidationRule
extension methods added in #130 only accept and IValidationState
, more usefully, they should accept any object/struct that implements that interface (including ValidationState
).
Describe the solution you'd like
Adding the following 2 overloads will significantly increase the usability of the API:
/// <summary>
/// Setup a validation rule with a general observable based on <see cref="IValidationState"/>.
/// </summary>
/// <typeparam name="TViewModel">ViewModel type.</typeparam>
/// <typeparam name="TValue">Validation observable type.</typeparam>
/// <param name="viewModel">ViewModel instance.</param>
/// <param name="validationObservable">Observable to define if the viewModel is valid or not.</param>
/// <returns>Returns a <see cref="ValidationHelper"/> object.</returns>
/// <remarks>
/// It should be noted that the observable should provide an initial value, otherwise that can result
/// in an inconsistent performance.
/// </remarks>
public static ValidationHelper ValidationRule<TViewModel, TValue>(
this TViewModel viewModel,
IObservable<TValue> validationObservable)
where TViewModel : IReactiveObject, IValidatableViewModel
where TValue : IValidationState
{
if (viewModel is null)
{
throw new ArgumentNullException(nameof(viewModel));
}
if (validationObservable is null)
{
throw new ArgumentNullException(nameof(validationObservable));
}
return viewModel.RegisterValidation(
new ObservableValidation<TViewModel, bool>(
validationObservable.Select(s => s as IValidationState)));
}
/// <summary>
/// Setup a validation rule with a general observable based on <see cref="IValidationState"/>.
/// </summary>
/// <typeparam name="TViewModel">ViewModel type.</typeparam>
/// <typeparam name="TViewModelProp">ViewModel property type.</typeparam>
/// <typeparam name="TValue">Validation observable type.</typeparam>
/// <param name="viewModel">ViewModel instance.</param>
/// <param name="viewModelProperty">ViewModel property referenced in viewModelObservableProperty.</param>
/// <param name="validationObservable">Observable to define if the viewModel is valid or not.</param>
/// <returns>Returns a <see cref="ValidationHelper"/> object.</returns>
/// <remarks>
/// It should be noted that the observable should provide an initial value, otherwise that can result
/// in an inconsistent performance.
/// </remarks>
public static ValidationHelper ValidationRule<TViewModel, TViewModelProp, TValue>(
this TViewModel viewModel,
Expression<Func<TViewModel, TViewModelProp>> viewModelProperty,
IObservable<TValue> validationObservable)
where TViewModel : IReactiveObject, IValidatableViewModel
where TValue : IValidationState
{
if (viewModel is null)
{
throw new ArgumentNullException(nameof(viewModel));
}
if (viewModelProperty is null)
{
throw new ArgumentNullException(nameof(viewModelProperty));
}
if (validationObservable is null)
{
throw new ArgumentNullException(nameof(validationObservable));
}
return viewModel.RegisterValidation(
new ObservableValidation<TViewModel, bool, TViewModelProp>(
viewModelProperty, validationObservable.Select(v => v as IValidationState)));
}
These will now accept observables such as IObservable<ValidationState
.
Describe alternatives you've considered
Without these methods consumers need to write code like this:
IObservable<ValidationState> observable = ...;
model.ValidationRule(observable.Select(v => v as IValidationState));
Describe suggestions on how to achieve the feature
Implementation above.
Describe the solution you'd like
Currently ValidationText
is mutable, allowing additional texts
to be added, or for the collection to be cleared. This functionality is not currently used in the library and has performance and thread-safety implications.
Describe alternatives you've considered
Having an alternative immutable implementation that can be used interchangeably with the existing mutable version, does not remove the thread-safety issues of accessing the collection whilst it may be being mutated.
Describe suggestions on how to achieve the feature
The Add()
and Clear()
methods should be removed, and the underlying list changed to a nullable array of _texts (null
for empty). Although this is a breaking change, it is worth it as it allows a static readonly ValidationText.Empty
singleton property to be added and reduces memory utilisation.
Ideally, the current constructors should be deprecated for a factory method that returns Empty
when supplied with an empty enumerable.
Additional context
Currently ReactiveUI.Validation only support the Android.Support Library most ReactiveUI Packages are updated to use AndroidX
It would be great if ReactiveUI.Validation supports AndroidX
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.