Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Add ValidationRule based on an Observable's value #116

Closed
thargy opened this issue Oct 12, 2020 · 3 comments
Closed

feature: Add ValidationRule based on an Observable's value #116

thargy opened this issue Oct 12, 2020 · 3 comments

Comments

@thargy
Copy link
Contributor

thargy commented Oct 12, 2020

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:

  1. Func<T, bool>, to determine if the item of type T is valid.
  2. 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:

  1. msg => msg != null;
  2. msg => msg;

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);
@thargy thargy changed the title feature: Add ValidationRule based in Observable feature: Add ValidationRule based on an Observable's value Oct 12, 2020
@worldbeater
Copy link
Collaborator

Added in #119 and should be available in the main branch now.

@thargy
Copy link
Contributor Author

thargy commented Oct 14, 2020

Having reviewed #118 and #119, I've added a comment to #118 about cleaning the API to remove the need for the problematic ContainsProperty overload (it sits more comfortably with those changes). #119 fully implements this feature request, so closing.

@github-actions
Copy link

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Nov 25, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants