Search code examples
c#.netsystem.reactivereactivexrx.net

Clamp the value of a BehaviorSubject


Is it possible to transform/validate the value of a ISubject<T> ?

E.g. I have a BehaviorSubject<double> zoomFactor = new(1); that I would like to be clamped between 0.1 and 10.

  • It should be possible to call zoomFactor.OnNext(Math.Clamp(newZoomFactor, 0.1, 10)), but doing so puts the responsibility on the caller, which I wish to avoid.
  • I could transform upon observation using zoomFactor.Select(newZoomFactor => Math.Clamp(newZoomFactor, 0.1, 10)), but:
    • This is outsourcing responsibility onto another caller again;
    • The original zoomFactor value wouldn't be changed here: imagine the user zooming out past the limit, and zooming back in, yet the rendered zoom is stuck to 10 during the time the actual value silently gets back into the clamped bounds internally...

Solution

  • How about creating a custom ISubject<T> implementation with the desirable behavior?

    class BehaviorTransformSubject<T> : ISubject<T>
    {
        private readonly BehaviorSubject<T> _subject;
        private readonly Func<T, T> _transform;
    
        public BehaviorTransformSubject(T value, Func<T, T> transform)
        {
            _subject = new BehaviorSubject<T>(value);
            _transform = transform;
        }
    
        public void OnNext(T value) => _subject.OnNext(_transform(value));
        public void OnCompleted() => _subject.OnCompleted();
        public void OnError(Exception error) => _subject.OnError(error);
        public IDisposable Subscribe(IObserver<T> o) => _subject.Subscribe(o);
    }
    

    Usage example:

    ISubject<double> zoomFactor = new BehaviorTransformSubject<double>(1.0,
        x => Math.Clamp(x, 0.1, 10.0));
    

    Alternative implementation: The OnNext method could be implemented alternatively like this:

    public void OnNext(T value)
    {
        T newValue;
        try { newValue = _transform(value); }
        catch (Exception ex) { _subject.OnError(ex); return; }
        _subject.OnNext(newValue);
    }
    

    This one handles differently a possible failure of the transform function. Instead of throwing the error directly back on the producer who invoked the OnNext method, it propagates it to the consumers of the subject, causing its irreversible termination (no more values will be propagated through this subject). I guess that the original OnNext implementation has the semantics that you are looking for, but I might be wrong.

    To be fair the Math.Clamp method never fails (according to the docs), so this distinction is mostly academic in your case.