Cysharp / R3

The new future of dotnet/reactive and UniRx.
MIT License
1.72k stars 70 forks source link

Confusion about migrating from ReactiveProperty #145

Closed maonaoda closed 4 months ago

maonaoda commented 4 months ago

I knew that R3 it the next ReactiveProperty,

Since there are no ObserveHasErrors, How do I need to express it using R3? It that Password.ErrorsChanged ? PasswordHasError = Password.ObserveHasErrors.CombineLatest(PasswordUnfocused, (x, y) => x && y).ToReactiveProperty().AddTo(Disposables);

※There is also ObserveValidationErrorMessage

neuecc commented 4 months ago

BindableReactiveProperty provides ErrorsChanged event. This is an event for XAML Binding, but can also be subscribed to manually. If you want to operate with Rx, you can convert this as a FromEvent.

maonaoda commented 4 months ago

@neuecc Hi, I am using Maui,

Is that be subscribed manually like this?

            Password = new BindableReactiveProperty<string>(string.Empty).EnableValidation(() => Password);
            PasswordHasError =
                Observable.EveryValueChanged(Password, x => x.HasErrors)
                .ToBindableReactiveProperty();
            PasswordErrorMessage =
                Observable.EveryValueChanged(Password, x => Password.GetErrors(null).OfType<ValidationResult>()?.FirstOrDefault()?.ErrorMessage)
                .ToBindableReactiveProperty();

ReactiveProperty`s using:

            Password = new ReactiveProperty<string>(string.Empty).SetValidateAttribute(() => Password).AddTo(Disposables);
            PasswordHasError = Password.ObserveHasErrors.ToReactiveProperty().AddTo(Disposables);
            PasswordErrorMessage = Password.ObserveValidationErrorMessage().ToReactiveProperty().AddTo(Disposables);
neuecc commented 4 months ago

First, R3 is not intended to port the full functionality of ReactiveProperty (more like ReactivePropertySlim, if anything). If that works, then it's good.

maonaoda commented 4 months ago

I added this method for myself. Thanks for your guidance.

    public static class BindableReactivePropertyExtensions
    {
        public static Observable<bool> ObserveHasErrors<T>(this BindableReactiveProperty<T> source)
        {
            return Observable.EveryValueChanged(source, x => x.HasErrors);
        }

        public static Observable<string?> ObserveValidationErrorMessage<T>(this BindableReactiveProperty<T> source)
        {
            return Observable.EveryValueChanged(source, x => x.GetErrors(null)?.OfType<ValidationResult>()?.FirstOrDefault()?.ErrorMessage);
        }
    }
maonaoda commented 4 months ago

The above code is constantly executing, and I changed it to this (FromEvent). There are slight differences of the actual action with ReactiveProperty, but it does not affect the final effect.

Although BindableReactivePropertysHasErrorsorGetErrors` can be used directly in Xaml, other processing is required in our app.

            PasswordHasError = Password.ErrorsChangedAsObservable()
                .CombineLatest(PasswordUnfocused, (_, y) => (Password.HasErrors && y))
                .ToBindableReactiveProperty();
            PasswordErrorMessage = Password.ErrorsChangedAsObservable().Select(_ => Password.GetErrorMessage())
                .ToBindableReactiveProperty();
        public static Observable<(object? sender, DataErrorsChangedEventArgs e)> ErrorsChangedAsObservable<T>(this BindableReactiveProperty<T> source)
        {
            return Observable.FromEventHandler<DataErrorsChangedEventArgs>(handler => source.ErrorsChanged += handler,
                handler => source.ErrorsChanged -= handler).AsObservable();
        }

        public static string? GetErrorMessage<T>(this BindableReactiveProperty<T> source)
        {
            return source.GetErrors(null)?.OfType<ValidationResult>()?.FirstOrDefault()?.ErrorMessage;
        }
maonaoda commented 4 months ago

Since R3 v1.1.1 released, I changed to ObservePropertyChanged to migrate form ReactiveProperty, Because DataErrorsChangedEvent just is ErrorsChangedEvent.

        public static Observable<bool> ObserveHasErrors<T>(this BindableReactiveProperty<T> source)
        {
            return source.ObservePropertyChanged(x => x.Value)
                .Select(_ => source.HasErrors);
        }

        public static Observable<string?> ObserveValidationErrorMessage<T>(this BindableReactiveProperty<T> source)
        {
            return source.ObservePropertyChanged(x => x.Value)
                .Select(_ => source.GetErrorMessage());
        }

        private static string? GetErrorMessage<T>(this BindableReactiveProperty<T> source)
        {
            return source.GetErrors(null)?.OfType<ValidationResult>()?.FirstOrDefault()?.ErrorMessage;
        }
maonaoda commented 4 months ago

May be v1.1.2 fixed Errors`s disappeance, that I can use FromEventHandler again ?

maonaoda commented 4 months ago

May be v1.1.2 fixed Errors`s disappeance, that I can use FromEventHandler again ?

Thats right, use FromEventHandler instead of ObservePropertyChanged, since that ObservePropertyChanged will handel every value changed.

maonaoda commented 4 months ago

@neuecc

One more thing. Because CombineLatest has the check of Value existence, when I use FromEventHandler like ↓, Unless calling Password.ForceNotify(); manually after,the using of FromEventHandler will not have value at the first time. and CombineLatest will not be excuted for the first time.

            PasswordHasError = Password
                //.ErrorsChangedAsObservable()
                .ObserveHasErrors()
                .CombineLatest(PasswordUnfocused, (_, y) => (Password.HasErrors && y))
                .ToBindableReactiveProperty();

(https://github.com/Cysharp/R3/blob/b72a8d8a8761908b903353fbe5a9cce5168f30c5/src/R3/Operators/CombineLatest.cs#L281-L297)

neuecc commented 4 months ago

you ca use Prepend for initial value

maonaoda commented 4 months ago

Prepend is OK.

        public static Observable<bool> ObserveHasErrors<T>(this BindableReactiveProperty<T> source)
        {
            return source
                .ErrorsChangedAsObservable()
                //.ObservePropertyChanged(x => x.Value)
                .Select(_ => source.HasErrors)
                .Prepend(true);
        }

        public static Observable<string?> ObserveValidationErrorMessage<T>(this BindableReactiveProperty<T> source, string defaultError)
        {
            return source
                .ErrorsChangedAsObservable()
                //.ObservePropertyChanged(x => x.Value)
                .Select(_ => source.GetErrorMessage())
                .Prepend(defaultError);
        }
maonaoda commented 4 months ago

Prepend is OK.

Take back this statement as .Prepend(defaultError); won`t set inner errors. so previouslyHasErrors is false, but i want it is true.

reuse ForceNotify() finally~