MIT License

技术解读

以下内容由 GitRead 中文社区 编辑团队基于项目 README 与官方资料整理, 结合中文开发者的常见使用场景补充技术选型建议。具体以项目官方文档为准。

深度解析:ReactiveUI.Validation 项目解析

项目背景与痛点

在现代的前端开发中,用户输入的校验是一个不可忽视的关键环节。确保每个字段的输入格式正确,不仅关乎用户体验,也是后端数据安全和完整性的重要保障。ReactiveUI 是一个在 .NET 平台上应用广泛的状态管理库,但考虑到表单输入校验的复杂性,社区推出了 ReactiveUI.Validation,一个专门为 ReactiveUI 应用程序提供丰富校验功能的辅助工具。

相比之下,此库不仅简化了校验逻辑的编写过程,还提供了一项核心功能——即时反馈。在传统的校验方式中,输入字段触发校验后,往往要等到整个表单提交才会触发反馈,而 ReactiveUI.Validation 能够在用户输入时实时反馈错误信息,大大提升了用户体验。

核心技术揭秘

ReactiveUI.Validation 实现即时反馈的能力来源于其巧妙的技术架构。ReactiveUI 的一切都是围绕声明式编程构建的,它使用 Rx (Reactive Extensions) 订阅输入流来观察和响应实时变化。因此,ReactiveUI.Validation 充分利用这一特性,为每个需要校验的字段绑定 Observatory 属性,该属性通过 .NET 的 IObservable 接口来触发,并实时监听输入的变化。

为了展示这一过程,我们构建了一个简单的数据流图(如图 1 所示)。

graph TD A[用户输入] --> B[Observatory订阅更改] B --> C{"输入是否为空"} C -->|"空"| D[显示错误提示] C -->|"非空"| E{"验证输入格式"} E -->|"格式正确"| F[输入通过校验] E -->|"格式错误"| G[显示错误提示]

图1: ReactiveUI.Validation 数据流图

这一过程的核心在于校验规则的设计和处理。ReactiveUI.Validation 封装了多种常用的校验器,包括必填校验、格式校验(如邮箱、手机号)和范围校验等基本类型,并通过链式调用来组织复杂的校验逻辑。

功能亮点与差异

相比其他常用校验库,ReactiveUI.Validation 的亮点在于其与 ReactiveUI 的无缝集成以及丰富的扩展性。声明式和反应式编程的特点,使其能够轻松处理复杂的动态校验逻辑,支持开发者通过配置参数来细调校验流程。

此外,得益于 Rx 的强大功能,ReactiveUI.Validation 支持复杂的异步校验机制。在端到端的校验过程中,不仅可以实现实时反馈,还能进行复杂的验证逻辑,如异步服务端数据校验,这在同类项目中并不多见。

应用场景与落地建议

ReactiveUI.Validation 被广泛应用于需要跨平台且对用户体验有高要求的 .NET 项目。例如,涉及复杂表单输入和快速反馈的应用,诸如 MVP 模式的移动或者桌面应用。

在落地生产环境时,开发者需要关注以下几点:

  • 性能优化:特别是面对大量复杂表单的场景,确保每一步校验逻辑的高效运行。
  • 错误处理:设计合理的错误反馈机制,确保用户能快速定位并解决输入问题。
  • 集成测试:在实际应用中,确保校验逻辑的正确性和适用性,进行详细的单元测试与集成测试。

综合评价

ReactiveUI.Validation 因其优雅的设计、优秀的响应式支持和强大的扩展性,为开发者提供了空前便捷的表单校验方案,特别适合那些追求高效且用户友好的 .NET 开发者。但其对编程思维和Rx API 的理解有一定要求,因此,良好的学习资料和实践经验是成功运用该项目的关键所在。

关于

GitHub 原始描述(英文)

Validation helpers for ReactiveUI-based apps.

描述取自 GitHub 官方 API,保持仓库作者书写的原文。

语言占比

C#

Release

README

NuGet Downloads Build Code Coverage License Slack Good First Issues Stars

ReactiveUI.Validation

Validation for ReactiveUI based solutions, functioning in a reactive way.

Read this in other languages: Español.

Core Team


Chris Pulman

London, UK


Glenn Watson

Melbourne, Australia

Alumni & History

ReactiveUI.Validation was originally developed by @jcmm33 as Vistian.Reactive.Validation, and then refactored and updated by Àlex Martínez Morón and the ReactiveUI community.

Supported Platforms

Platform Targets
.NET net8.0, net9.0, net10.0
.NET Framework net462, net472, net481
Windows net8.0-windows10.0.19041.0, net9.0-windows10.0.19041.0, net10.0-windows10.0.19041.0
Android (MAUI) net9.0-android, net10.0-android

NuGet Packages

Install the following package into your class library and into a platform-specific project.

Platform Package NuGet
Any Platform ReactiveUI.Validation CoreBadge
AndroidX (MAUI) ReactiveUI.Validation.AndroidX DroXBadge

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.
using ReactiveUI.Validation.Extensions;

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.
using ReactiveUI.Validation.Extensions;

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);
        });
    }
}

Example with Android Extensions

There are extensions methods for 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.Extensions and install ReactiveUI.Validation.AndroidX:

dotnet add package ReactiveUI.Validation.AndroidX
// This using directive makes Android-specific extensions available.
using ReactiveUI.Validation.Extensions;

public class SignUpActivity : ReactiveAppCompatActivity<SignUpViewModel>
{
    // The Android native text boxes declared in an .axml file.
    public TextInputEditText Password { get; set; }
    public TextInputEditText ConfirmPassword { get; set; }

    // The layouts wrapping the text boxes declared in an .axml file.
    public TextInputLayout PasswordField { get; set; }
    public TextInputLayout ConfirmPasswordField { 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, x => x.Password, x => x.Password.Text);
        this.Bind(ViewModel, x => x.ConfirmPassword, x => x.ConfirmPassword.Text);

        // Bind any validations which reference the Password property
        // to the Error property of the TextInputLayout control.
        this.BindValidation(ViewModel, x => x.Password, PasswordField);
        this.BindValidation(ViewModel, x => x.ConfirmPassword, ConfirmPasswordField);
    }
}

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.

using ReactiveUI.Validation.Extensions;

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);
    }
}

Note Keep in mind that INotifyDataErrorInfo is only supported via XAML binding. ReactiveUI binding doesn’t use the inbuilt classes of WPF.

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.");

Custom Formatters

You can pass an instance of IValidationTextFormatter<T> to a call to BindValidation if you’d like to override the default SingleLineFormatter used in the validation library. The SingleLineFormatter accepts a separator char and uses whitespace by default, so the code snippet below shows how to use a non-default separator char:

// 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 custom 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 custom IValidationTextFormatter implementation.
var formatter = new ConstFormatter("The input is invalid.");
this.BindValidation(ViewModel, x => x.ErrorLabel.Text, formatter)
    .DisposeWith(disposables);

If you’d like to override the IValidationTextFormatter<string> used in ReactiveUI.Validation by default, register an instance of IValidationTextFormatter<string> into Locator.CurrentMutable before your app starts. This could be useful in cases when your app needs localization and you wish to pass message keys instead of messages to ValidationRule calls.

// Register a singleton instance of IValidationTextFormatter<string> into Splat.Locator.
Locator.CurrentMutable.RegisterConstant(new CustomFormatter(), typeof(IValidationTextFormatter<string>));

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.

Contribute

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

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.

评论

暂无评论