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
.
zoomFactor.OnNext(Math.Clamp(newZoomFactor, 0.1, 10))
, but doing so puts the responsibility on the caller, which I wish to avoid.zoomFactor.Select(newZoomFactor => Math.Clamp(newZoomFactor, 0.1, 10))
, but:
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...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.