Validation helpers for ReactiveUI-based apps.
Перейти к файлу
Craig Dean 053ea391bc
housekeeping: Update README.md (#133)
* Update README.md

Clean up pass, to improve intelligibility and reflect the latest API additions more clearly.

* Update README.md

Co-authored-by: Artyom V. Gorchakov <worldbeater-dev@yandex.ru>

Co-authored-by: Glenn <5834289+glennawatson@users.noreply.github.com>
Co-authored-by: Artyom V. Gorchakov <worldbeater-dev@yandex.ru>
2020-10-18 12:44:40 +11:00
.github housekeeping: Fix invalid environment in github action (#113) 2020-10-12 13:30:26 +11:00
media Added logo. 2019-02-24 12:35:07 +01:00
samples feature: Use IValidationState Instead of ValidationState (#130) 2020-10-17 17:24:25 +03:00
src feature: Use IValidationState Instead of ValidationState (#130) 2020-10-17 17:24:25 +03:00
.editorconfig housekeeping: Update to the latest msbuild.sdk.extras/pipelines 2019-08-06 04:05:58 +10:00
.gitattributes Added an analyzers, support for multiple validation and added Xamarin.Forms sample. (#5) 2019-03-19 12:38:12 -07:00
.gitignore housekeeping: Update to the latest msbuild.sdk.extras/pipelines 2019-08-06 04:05:58 +10:00
LICENSE Added an analyzers, support for multiple validation and added Xamarin.Forms sample. (#5) 2019-03-19 12:38:12 -07:00
README.md housekeeping: Update README.md (#133) 2020-10-18 12:44:40 +11:00
version.json housekeeping: Release version 1.7 (#125) 2020-10-15 22:13:54 +03:00

README.md

NuGet Stats Build Code Coverage #yourfirstpr Downloads Slack

ReactiveUI.Validation

Validation for ReactiveUI based solutions, functioning in a reactive way. Based on jcmm33's Vistian.Reactive.Validation.

NuGet Packages

Install the following package into you class library and platform-specific project. ReactiveUI.Validation package supports all platforms, including .NET Framework, .NET Standard, .NET Core, MonoAndroid, Tizen, UAP, Xamarin.iOS, Xamarin.Mac, Xamarin.TVOS.

Platform ReactiveUI Package NuGet
Any ReactiveUI.Validation CoreBadge

How to Use

  • For ViewModels which need validation, implement IValidatableViewModel.
  • Add validation rules to the ViewModel using the ValidationRule extension methods.
  • Bind to the validation rules in the View via BindValidation or INotifyDataErrorInfo.

Example

  1. Decorate existing ViewModel with IValidatableViewModel, which has a single member, ValidationContext. The ValidationContext contains all of the functionality surrounding the validation of the ViewModel. Most access to the specification of validation rules is performed through extension methods on the IValidatableViewModel interface. Then, add validation to the ViewModel.
public class SampleViewModel : ReactiveObject, IValidatableViewModel
{
    public SampleViewModel()
    {
        // Creates the validation for the Name property.
        this.ValidationRule(
            viewModel => viewModel.Name,
            name => !string.IsNullOrWhiteSpace(name),
            "You must specify a valid name");
    }

    public ValidationContext ValidationContext { get; } = new ValidationContext();

    private string _name;
    public string Name
    {
        get => _name;
        set => this.RaiseAndSetIfChanged(ref _name, value);
    }
}

For more complex validation scenarios there are several more overloads of the ValidationRule extension method that accept observables. These allow validation to occur asynchronously, and allows complex chains of observables to be combined to produce validation results.

The simplest accepts an IObservable<bool> where the observed boolean indicates whether the ValidationRule is valid or not. The overload accepts a message which is used when the observable produces a false (invalid) result.

IObservable<bool> passwordsObservable =
    this.WhenAnyValue(
        x => x.Password,
        x => x.ConfirmPassword,
        (password, confirmation) => password == confirmation);

this.ValidationRule(
    vm => vm.ConfirmPassword,
    passwordsObservable,
    "Passwords must match.");

Any existing observables can be used to drive a ValidationRule using the extension method overload that accepts an arbitrary IObservable<TState> streams of events. The overload accepts a custom validation function that is supplied with the latest TState, and a custom error message function, responsible for formatting the latest TState object. The syntax for this looks as follows:

// IObservable<{ Password, Confirmation }>
var passwordsObservable =
    this.WhenAnyValue(
        x => x.Password,
        x => x.ConfirmPassword,
        (password, confirmation) =>
            new { Password = password, Confirmation = confirmation });

this.ValidationRule(
    vm => vm.ConfirmPassword,
    passwordsObservable,
    state => state.Password == state.Confirmation,
    state => $"Passwords must match: {state.Password} != {state.Confirmation}");

Note The function to extract a message (messageFunc) is only invoked if the function to establish validity (isValidFunc) returns false, otherwise the message is set to string.Empty.

Finally, you can directly supply an observable that streams any object (or struct) that implements IValidationState; or you can use the ValidationState base class which already implements the interface. As the resulting object is stored directly against the context without further transformation, this can be the most performant approach:

IObservable<IValidationState> usernameNotEmpty =
    this.WhenAnyValue(x => x.UserName)
        .Select(name => string.IsNullOrEmpty(name) 
            ? new ValidationState(false, "The username must not be empty")
            : ValidationState.Valid);

this.ValidationRule(vm => vm.UserName, usernameNotEmpty);

Note As a valid ValidationState does not really require a message, there is a singleton ValidationState.Valid property that you are encouraged to use to indicate a valid state whenever possible, to reduce memory allocations.

  1. Add validation presentation to the View.
public class SampleView : ReactiveContentPage<SampleViewModel>
{
    public SampleView()
    {
        InitializeComponent();
        this.WhenActivated(disposables =>
        {
            this.Bind(ViewModel, vm => vm.Name, view => view.Name.Text)
                .DisposeWith(disposables);

            // Bind any validations that reference the Name property 
            // to the text of the NameError UI control.
            this.BindValidation(ViewModel, vm => vm.Name, view => view.NameError.Text)
                .DisposeWith(disposables);

            // Bind any validations attached to this particular view model
            // to the text of the FormErrors UI control.
            this.BindValidation(ViewModel, view => view.FormErrors.Text)
                .DisposeWith(disposables);
        });
    }
}

Note Name is an <Entry />, NameError is a <Label />, and FormErrors is a <Label /> as well. All these controls are from the Xamarin.Forms library.

Example with Android Extensions

There are extensions methods for Xamarin.Android and its Material design control TextInputLayout. These extensions use internally the Error property from the TextInputLayout control, allowing you to implement a fully native behavior to showing validation errors. To use these extensions you must import ReactiveUI.Validation.Platforms.Android:

// This using directive makes Android-specific extensions available.
using ReactiveUI.Validation.Platforms.Android;

namespace SampleApp.Activities
{
    public class SampleActivity : ReactiveAppCompatActivity<SampleViewModel>
    {
        public TextInputEditText Name { get; set; }

        public TextInputLayout NameLayout { get; set; }

        protected override void OnCreate (Bundle bundle)
        {
            base.OnCreate(bundle);
            SetContentView(Resource.Layout.Main);

            // The WireUpControls method is a magic ReactiveUI utility method for Android, see:
            // https://www.reactiveui.net/docs/handbook/data-binding/xamarin-android/wire-up-controls
            this.WireUpControls();

            this.Bind(ViewModel, vm => vm.Name, view => view.Name.Text);

            // Bind any validations which reference the Name property 
            // to the Error property of the TextInputLayout control.
            this.BindValidation(ViewModel, vm => vm.Name, NameLayout);
        }
    }
}

INotifyDataErrorInfo Support

For those platforms that support the INotifyDataErrorInfo interface, ReactiveUI.Validation provides a helper base class named ReactiveValidationObject. The helper class implements both the IValidatableViewModel interface and the INotifyDataErrorInfo interface. It listens to any changes in the ValidationContext and invokes INotifyDataErrorInfo events.

public class SampleViewModel : ReactiveValidationObject
{
    public SampleViewModel()
    {
        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);
    }
}

When using a ValidationRule overload that accepts an observable, please remember to supply the property which the validation rule is targeting as the first argument. Otherwise it is not possible for INotifyDataErrorInfo to conclude which property the error message is for.

this.ValidationRule(
    vm => vm.ConfirmPassword,
    passwordsObservable,
    "Passwords must match.");

Capabilities

In essence, ReactiveUI.Validation is a relatively simple model of the ValidationContext containing a list of IValidationComponent instances. An IValidationComponent provides an observable of IValidationState. Whenever validation state changes (either a transition of validity) or ValidationText changes, then a new value is pushed out.

  1. Rules can be composed of single or multiple properties along with more generic Observables.
  2. Validation text can encapsulate both valid and invalid states.
  3. Binding can occur to either a View or an action.
  4. Validation text can reference either the ViewModel or properties which comprise the validation rule e.g. include text entered as part of validation message.
  5. Validation text output can be adjusted using custom formatters, not only allowing for single & multiline output but also for platforms like Android it should be possible to achieve richer renderings i.e. Bold/italics.
// This formatter is based on the default SingleLineFormatter but uses a custom separator char.
var formatter = new SingleLineFormatter(Environment.NewLine);
this.BindValidation(ViewModel, x => x.ErrorLabel.Text, formatter)
    .DisposeWith(disposables);

The simplest possible IValidationTextFormatter<TOut> implementation may look like this one.

private class ConstFormatter : IValidationTextFormatter<string>
{
    private readonly string _text;

    public ConstFormatter(string text = "The input is invalid.") => _text = text;

    public string Format(ValidationText validationText) => _text;
}

// This formatter is based on a custsom IValidationTextFormatter implementation.
var formatter = new ConstFormatter("The input is invalid.");
this.BindValidation(ViewModel, x => x.ErrorLabel.Text, formatter)
    .DisposeWith(disposables);

Feedback

Please use GitHub issues for questions, comments, or bug reports.

Authors

Contribute

ReactiveUI.Validation is developed under an OSI-approved open source license, making it freely usable and distributable, even for commercial use. Because of our Open Collective model for funding and transparency, we are able to funnel support and funds through to our contributors and community. We ❤ the people who are involved in this project, and wed love to have you on board, especially if you are just getting started or have never contributed to open-source before.

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

Code released under the MIT license.