using System;
namespace PropertyFacadeExample.ViewModel.Features
{
public sealed class EditWorkingAttendanceViewModel : UXViewModel, ITracksChanges
{
public ChangeTracker ChangeTracker { get; } = new ChangeTracker();
public EditWorkingAttendanceViewModel()
{
// Also includes change tracking support, which is optional.
// DisposeWith registers the subscription with DisposableTracker.
// This is necessary if you expect the domain model to live longer than the viewmodel.
// When you close the viewmodel, call DisposableTracker.Dispose().
//
// You can dispose a PropertyFacade as many times as you need; this only disposes the underlying
// subscription if one is present. Likewise you can re-use and re-dispose the tracker.
//
// Take care when creating and registering regular Rx subscriptions here in the constructor if you
// intend to reuse the viewmodel after Dispose. If you dispose those subscriptions, they're gone.
_adminA = this.PropFacade(vm => vm.AdminA).TrackChanges(this).DisposeWith(this);
_adminB = this.PropFacade(vm => vm.AdminB).TrackChanges(this).DisposeWith(this);
_nonSalary = this.PropFacade(vm => vm.NonSalary).TrackChanges(this).DisposeWith(this);
_salary = this.PropFacade(vm => vm.Salary).TrackChanges(this).DisposeWith(this);
_travel = this.PropFacade(vm => vm.Travel).TrackChanges(this).DisposeWith(this);
_leave = this.PropFacade(vm => vm.Leave).TrackChanges(this).DisposeWith(this);
_total = this.ReadOnlyPropFacade(vm => vm.Total).DisposeWith(this);
}
public decimal AdminA { get => _adminA.Value; set => _adminA.Value = value; }
private readonly PropertyFacade<decimal> _adminA;
public decimal AdminB { get => _adminB.Value; set => _adminB.Value = value; }
private readonly PropertyFacade<decimal> _adminB;
public decimal NonSalary { get => _nonSalary.Value; set => _nonSalary.Value = value; }
private readonly PropertyFacade<decimal> _nonSalary;
public decimal Salary { get => _salary.Value; set => _salary.Value = value; }
private readonly PropertyFacade<decimal> _salary;
public decimal Travel { get => _travel.Value; set => _travel.Value = value; }
private readonly PropertyFacade<decimal> _travel;
public decimal Leave { get => _leave.Value; set => _leave.Value = value; }
private readonly PropertyFacade<decimal> _leave;
public decimal Total => _total.Value;
private readonly ReadOnlyPropertyFacade<decimal> _total;
public void Load(Domain.Models.WorkingAttendance model)
{
if (model is null)
{
throw new ArgumentNullException(nameof(model));
}
// PropertyFacade supports hot-swapping subscriptions.
// If we want to treat the domain model as more-or-less discardable while keeping the viewmodel alive,
// we can replace an older model with a new one.
// We can also use ConvertOneWay or ConvertTwoWay extension methods from FacadeConverters.cs to convert value types
// if the PropertyFacade type and the domain property type do not match. We may want to do this if we
// need to "play nice" with a UI control that doesn't like the property on the domain.
// To use this, place ConvertTwoWay() before ToProperty().
model.AdminATime.ToProperty(this, _adminA);
model.AdminBTime.ToProperty(this, _adminB);
model.NonSalaryTime.ToProperty(this, _nonSalary);
model.SalaryTime.ToProperty(this, _salary);
model.TravelTime.ToProperty(this, _travel);
model.LeaveTime.ToProperty(this, _leave);
model.TotalTime.ToProperty(this, _total);
}
}
}
using PropertyFacadeExample.Domain;
using System;
using System.Collections.Generic;
using System.Reactive.Subjects;
namespace PropertyFacadeExample.ViewModel
{
public sealed class ChangeTracker : IHasChanges
{
private readonly List<IHasChanges> _subjects = new List<IHasChanges>();
public IReadOnlyValueObservable<bool> HasChanges { get; }
private readonly BehaviorSubject<bool> _hasChangesSubject = new BehaviorSubject<bool>(default);
private int TrueCount
{
get => _trueCount;
set
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), $"Value must not be negative ({value}).");
}
_trueCount = value;
}
}
private int _trueCount;
public ChangeTracker()
{
HasChanges = _hasChangesSubject.ToCached();
}
public void Add(IHasChanges subject)
{
if (subject is null)
{
throw new ArgumentNullException(nameof(subject));
}
_subjects.Add(subject);
bool init = true;
subject.HasChanges.Subscribe(hasChanges =>
{
// Do not count no-changes when attaching to the observable.
if (init && !hasChanges)
{
return;
}
TrueCount += hasChanges ? 1 : -1;
if (TrueCount == 0)
{
_hasChangesSubject.OnNext(false);
}
else if (TrueCount == 1 && hasChanges)
{
_hasChangesSubject.OnNext(true);
}
});
init = false;
}
public void ResetValue()
{
foreach (var facade in _subjects)
{
facade.ResetValue();
}
}
public void UpdateOriginalValue()
{
foreach (var facade in _subjects)
{
facade.UpdateOriginalValue();
}
}
}
}
using System;
using System.Collections.Generic;
namespace PropertyFacadeExample.ViewModel
{
/// <summary>
/// Tracks a list of <see cref="IDisposable"/> objects. Prefer this over <see cref="CompositeDisposable"/>
/// to avoid unnecessary allocations where no items need to be tracked.
/// </summary>
public sealed class DisposableTracker
{
private List<IDisposable> Disposables { get; set; }
public void Add(IDisposable disposable)
{
if (disposable is null)
{
throw new ArgumentNullException(nameof(disposable));
}
if (Disposables is null)
{
Disposables = new List<IDisposable>();
}
Disposables.Add(disposable);
}
/// <summary>
/// Disposes all tracked items and removes them from the list.
/// </summary>
public void Dispose()
{
if (!(Disposables is null))
{
foreach (var disposable in Disposables)
{
disposable.Dispose();
}
Disposables.Clear();
}
}
}
}
using System;
using System.Reactive.Linq;
using System.Reactive.Subjects;
namespace PropertyFacadeExample.ViewModel
{
public abstract class OneWayFacadeConverter<TSource, TFacade>
{
public abstract TFacade SourceToFacade(TSource sourceValue);
}
public abstract class TwoWayFacadeConverter<TSource, TFacade> : OneWayFacadeConverter<TSource, TFacade>
{
public abstract TSource FacadeToSource(TFacade facadeValue);
}
public class ObservableFacadeConverterAdapter<TSource, TFacade> : IObservable<TFacade>
{
private readonly IObservable<TSource> _observable;
private readonly OneWayFacadeConverter<TSource, TFacade> _converter;
public IDisposable Subscribe(IObserver<TFacade> observer) =>
_observable
.Select(sourceValue => _converter.SourceToFacade(sourceValue))
.Subscribe(observer);
public ObservableFacadeConverterAdapter(IObservable<TSource> observable, OneWayFacadeConverter<TSource, TFacade> converter)
{
_observable = observable ?? throw new ArgumentNullException(nameof(observable));
_converter = converter ?? throw new ArgumentNullException(nameof(converter));
}
}
public class SubjectFacadeConverterAdapter<TSource, TFacade> : ObservableFacadeConverterAdapter<TSource, TFacade>, ISubject<TFacade>
{
private readonly ISubject<TSource> _subject;
private readonly TwoWayFacadeConverter<TSource, TFacade> _converter;
public void OnNext(TFacade value) => _subject.OnNext(_converter.FacadeToSource(value));
public void OnError(Exception error) => _subject.OnError(error);
public void OnCompleted() => _subject.OnCompleted();
public SubjectFacadeConverterAdapter(ISubject<TSource> subject, TwoWayFacadeConverter<TSource, TFacade> converter)
: base(subject, converter)
{
_subject = subject ?? throw new ArgumentNullException(nameof(subject));
_converter = converter ?? throw new ArgumentNullException(nameof(converter));
}
}
public static class FacadeConverterExtensions
{
private sealed class OneWayFuncConverter<TSource, TFacade> : OneWayFacadeConverter<TSource, TFacade>
{
private readonly Func<TSource, TFacade> _sourceToFacade;
public override TFacade SourceToFacade(TSource sourceValue) => _sourceToFacade.Invoke(sourceValue);
public OneWayFuncConverter(Func<TSource, TFacade> sourceToFacade)
{
_sourceToFacade = sourceToFacade ?? throw new ArgumentNullException(nameof(sourceToFacade));
}
}
private sealed class TwoWayFuncConverter<TSource, TFacade> : TwoWayFacadeConverter<TSource, TFacade>
{
private readonly Func<TSource, TFacade> _sourceToFacade;
private readonly Func<TFacade, TSource> _facadeToSource;
public override TFacade SourceToFacade(TSource sourceValue) => _sourceToFacade.Invoke(sourceValue);
public override TSource FacadeToSource(TFacade facadeValue) => _facadeToSource.Invoke(facadeValue);
public TwoWayFuncConverter(Func<TSource, TFacade> sourceToFacade, Func<TFacade, TSource> facadeToSource)
{
_sourceToFacade = sourceToFacade ?? throw new ArgumentNullException(nameof(sourceToFacade));
_facadeToSource = facadeToSource ?? throw new ArgumentNullException(nameof(facadeToSource));
}
}
public static SubjectFacadeConverterAdapter<TSource, TFacade> ConvertTwoWay<TSource, TFacade>(
this ISubject<TSource> source,
Func<TSource, TFacade> sourceToFacade,
Func<TFacade, TSource> facadeToSource) =>
new SubjectFacadeConverterAdapter<TSource, TFacade>(
subject: source,
converter: new TwoWayFuncConverter<TSource, TFacade>(
sourceToFacade: sourceToFacade,
facadeToSource: facadeToSource));
public static ObservableFacadeConverterAdapter<TSource, TFacade> ConvertOneWay<TSource, TFacade>(
this IObservable<TSource> source,
Func<TSource, TFacade> sourceToFacade) =>
new ObservableFacadeConverterAdapter<TSource, TFacade>(
observable: source,
converter: new OneWayFuncConverter<TSource, TFacade>(
sourceToFacade: sourceToFacade));
public static SubjectFacadeConverterAdapter<TSource, TFacade> ConvertTwoWay<TSource, TFacade>(
this ISubject<TSource> source,
TwoWayFacadeConverter<TSource, TFacade> converter) =>
new SubjectFacadeConverterAdapter<TSource, TFacade>(source, converter);
public static ObservableFacadeConverterAdapter<TSource, TFacade> ConvertOneWay<TSource, TFacade>(
this IObservable<TSource> source,
OneWayFacadeConverter<TSource, TFacade> converter) =>
new ObservableFacadeConverterAdapter<TSource, TFacade>(source, converter);
}
}
using PropertyFacadeExample.Domain;
using System;
using System.Reactive.Subjects;
namespace PropertyFacadeExample.ViewModel
{
/// <summary>
/// Encapsulates an observable or subject to provide an efficient way to get or set the latest value.
/// </summary>
/// <typeparam name="T"></typeparam>
public class FacadeValueCache<T> : IObservable<T>, IDisposable
{
private IDisposable _disposable;
private IObservable<T> _observable;
private Func<T> _getValueFunc;
private Action<T> _setValueFunc;
public bool CanSet => !(_setValueFunc is null);
public bool HasObservable => !(_observable is null);
public void Attach(IObservable<T> obs)
{
if (obs is null)
{
throw new ArgumentNullException(nameof(obs));
}
Dispose();
if (obs is IReadOnlyValueObservable<T> iObsVal)
{
_observable = iObsVal;
_getValueFunc = () => iObsVal.Value;
}
else if (obs is BehaviorSubject<T> behSub)
{
_observable = behSub;
_getValueFunc = () => behSub.Value;
}
else
{
var cached = obs.ToCached();
_observable = cached;
_disposable = cached;
_getValueFunc = () => cached.Value;
}
if (obs is IObserver<T> iSub)
{
_setValueFunc = value => iSub.OnNext(value);
}
}
private void AssertHasObservable()
{
if (_observable is null)
{
throw new InvalidOperationException("No observable has been attached.");
}
}
public T GetValue()
{
AssertHasObservable();
return _getValueFunc.Invoke();
}
public void SetValue(T value)
{
AssertHasObservable();
if (CanSet)
{
_setValueFunc.Invoke(value);
}
else
{
throw new InvalidOperationException("The observable does not implement IObserver.");
}
}
public IDisposable Subscribe(IObserver<T> observer)
{
AssertHasObservable();
return _observable.Subscribe(observer);
}
public void Dispose()
{
_disposable?.Dispose();
_observable = null;
_getValueFunc = null;
_setValueFunc = null;
}
}
}
using PropertyFacadeExample.Domain;
namespace PropertyFacadeExample.ViewModel
{
public interface IHasChanges
{
IReadOnlyValueObservable<bool> HasChanges { get; }
void ResetValue();
void UpdateOriginalValue();
}
}
namespace PropertyFacadeExample.ViewModel
{
public interface ITracksChanges
{
ChangeTracker ChangeTracker { get; }
}
}
using System.Collections.Generic;
using System;
namespace PropertyFacadeExample
{
public sealed class NullOrEmptyStringEqualityComparer : EqualityComparer<string>
{
public static new NullOrEmptyStringEqualityComparer Default { get; } = new NullOrEmptyStringEqualityComparer(StringComparer.InvariantCulture);
public IEqualityComparer<string> InnerComparer { get; }
public NullOrEmptyStringEqualityComparer(IEqualityComparer<string> innerComparer)
{
InnerComparer = innerComparer ?? throw new ArgumentNullException(nameof(innerComparer));
}
public override bool Equals(string x, string y)
{
if (string.IsNullOrEmpty(x) && string.IsNullOrEmpty(y))
{
return true;
}
else
{
return InnerComparer.Equals(x, y);
}
}
public override int GetHashCode(string obj) => InnerComparer.GetHashCode(obj);
}
}
using PropertyFacadeExample.Domain;
using System;
namespace PropertyFacadeExample.ViewModel
{
public class PropertyFacade<T> : ReadOnlyPropertyFacade<T>, IHasChanges
{
public new T Value
{
get => base.Value;
set
{
if (IsDisposed)
{
throw new InvalidOperationException("The subscription for this observable has been disposed. Attach a new subscription by calling Observe.");
}
if (_valueCache is null)
{
throw new InvalidOperationException("The observable has not been set yet. Attach a new subscription by calling Observe.");
}
_valueCache.SetValue(value);
}
}
public PropertyFacade(UXViewModel viewModel, string propertyName)
: base(viewModel, propertyName)
{ }
public PropertyFacade(UXViewModel viewModel, IValueObservable<T> observable, string propertyName)
: base(viewModel, observable, propertyName)
{ }
public void ResetValue() => Value = OriginalValue;
}
}
using PropertyFacadeExample.Domain;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reactive.Linq;
using System.Reactive.Subjects;
namespace PropertyFacadeExample.ViewModel
{
[DebuggerDisplay("(PropertyFacade) {Value}")]
public class ReadOnlyPropertyFacade<T> : IDisposable
{
private readonly UXViewModel _viewModel;
private readonly string _propertyName;
protected FacadeValueCache<T> _valueCache;
private IDisposable _subscription;
private T _oldValue;
public bool IsDisposed { get; private set; }
public T Value => _valueCache.HasObservable ? _valueCache.GetValue() : default;
public T OriginalValue { get; protected set; }
public IReadOnlyValueObservable<bool> HasChanges { get; }
private readonly BehaviorSubject<bool> _hasChangesSubject = new BehaviorSubject<bool>(false);
public bool SuppressDebugOutput { get; set; }
public ReadOnlyPropertyFacade(UXViewModel viewModel, string propertyName)
{
_viewModel = viewModel ?? throw new ArgumentNullException(nameof(viewModel));
_propertyName = propertyName;
_valueCache = new FacadeValueCache<T>();
HasChanges = _hasChangesSubject.ToCached();
}
public ReadOnlyPropertyFacade(UXViewModel viewModel, IReadOnlyValueObservable<T> observable, string propertyName)
: this(viewModel, propertyName)
{
Observe(observable);
}
/// <summary>
/// Attach the facade to an observable. If an observable is already attached, the subscription is disposed and the new one is attached in its place.
/// </summary>
/// <param name="newObservable"></param>
/// <param name="equalityComparer"></param>
public void Observe(IObservable<T> newObservable, IEqualityComparer<T> equalityComparer = null)
{
if (newObservable is null)
{
throw new ArgumentNullException(nameof(newObservable));
}
Dispose();
_valueCache.Attach(newObservable);
OriginalValue = _valueCache.GetValue();
DebugMessage("PropertyFacade: New observable mounted on viewmodel='{0}', property='{1}', OriginalValue='{2}'", _viewModel, _propertyName, OriginalValue);
equalityComparer = equalityComparer ?? GetEqualityComparer<T>();
IsDisposed = false;
_subscription = _valueCache.Subscribe(newValue =>
{
DebugMessage("PropertyFacade: Received value on viewmodel='{0}', property='{1}', value='{2}'", _viewModel, _propertyName, newValue);
if (!equalityComparer.Equals(_oldValue, newValue))
{
// Only pump HasChanges if the new HasChanges value is different.
bool changed = !equalityComparer.Equals(OriginalValue, newValue);
if (_hasChangesSubject.Value != changed)
{
_hasChangesSubject.OnNext(changed);
}
RaisePropertyChanged(newValue);
}
});
}
private IEqualityComparer<TValue> GetEqualityComparer<TValue>()
{
if (typeof(TValue) == typeof(string))
{
return (IEqualityComparer<TValue>)(IEqualityComparer<string>)NullOrEmptyStringEqualityComparer.Default;
}
else
{
return EqualityComparer<TValue>.Default;
}
}
private void RaisePropertyChanged(T newValue)
{
DebugMessage("PropertyFacade: RaisePropertyChanging: '{0}', property='{1}', old='{2}', new='{3}'", _viewModel, _propertyName, _oldValue, newValue);
_viewModel.RaisePropertyChanging(_propertyName);
var oldTemp = _oldValue;
_oldValue = newValue;
DebugMessage("PropertyFacade: RaisePropertyChanged: '{0}', property='{1}', old='{2}', new='{3}'", _viewModel, _propertyName, oldTemp, newValue);
_viewModel.RaisePropertyChanged(_propertyName);
}
public void UpdateOriginalValue()
{
OriginalValue = Value;
if (HasChanges.Value)
{
_hasChangesSubject.OnNext(false);
}
}
/// <summary>
/// Disconnects any existing subscription. If none exists, no action is taken.
/// </summary>
public void Dispose()
{
IsDisposed = true;
_subscription?.Dispose();
_valueCache.Dispose();
}
[DebuggerStepThrough]
protected void DebugMessage(string message, params object[] args)
{
if (!SuppressDebugOutput && Debugger.IsAttached)
{
Debug.WriteLine(message, args);
}
}
}
}
using System.ComponentModel;
namespace PropertyFacadeExample.ViewModel
{
/// <summary>
/// A base view model implementation. If we were using ReactiveUI, this would also extend from ReactiveObject.
/// </summary>
public abstract class UXViewModel : INotifyPropertyChanged, INotifyPropertyChanging
{
public event PropertyChangedEventHandler PropertyChanged;
public event PropertyChangingEventHandler PropertyChanging;
public void RaisePropertyChanged(PropertyChangedEventArgs args) => PropertyChanged?.Invoke(this, args);
public void RaisePropertyChanged(string propertyName) => RaisePropertyChanged(new PropertyChangedEventArgs(propertyName));
public void RaisePropertyChanging(PropertyChangingEventArgs args) => PropertyChanging?.Invoke(this, args);
public void RaisePropertyChanging(string propertyName) => RaisePropertyChanging(new PropertyChangingEventArgs(propertyName));
public DisposableTracker DisposableTracker { get; } = new DisposableTracker();
}
}
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace PropertyFacadeExample.ViewModel
{
public static class UXViewModelExtensions
{
public static PropertyFacade<T> PropFacade<T, TViewModel>(this TViewModel vm, Expression<Func<TViewModel, T>> property)
where TViewModel : UXViewModel
{
if (vm is null)
{
throw new ArgumentNullException(nameof(vm));
}
if (property is null)
{
throw new ArgumentNullException(nameof(property));
}
var memberExpr = (MemberExpression)property.Body;
string propName = memberExpr.Member.Name;
return new PropertyFacade<T>(vm, propName);
}
public static ReadOnlyPropertyFacade<T> ReadOnlyPropFacade<T, TViewModel>(this TViewModel vm, Expression<Func<TViewModel, T>> property)
where TViewModel : UXViewModel
{
if (property is null)
{
throw new ArgumentNullException(nameof(property));
}
var memberExpr = (MemberExpression)property.Body;
string propName = memberExpr.Member.Name;
return new ReadOnlyPropertyFacade<T>(vm, propName);
}
public static void ToProperty<T>(this IObservable<T> observable, UXViewModel viewModel, ReadOnlyPropertyFacade<T> propertyFacade, IEqualityComparer<T> equalityComparer = null)
{
if (observable is null)
{
throw new ArgumentNullException(nameof(observable));
}
if (viewModel is null)
{
throw new ArgumentNullException(nameof(viewModel));
}
if (propertyFacade is null)
{
throw new ArgumentNullException(nameof(propertyFacade), "The property facade has not been initialized yet.");
}
propertyFacade.Observe(observable, equalityComparer);
viewModel.DisposableTracker.Add(propertyFacade);
}
public static T DisposeWith<T>(this T disposable, UXViewModel viewModel) where T : class, IDisposable
{
if (disposable is null)
{
throw new ArgumentNullException(nameof(disposable));
}
if (viewModel is null)
{
throw new ArgumentNullException(nameof(viewModel));
}
viewModel.DisposableTracker.Add(disposable);
return disposable;
}
public static T TrackChanges<T>(this T subject, ChangeTracker changeTracker)
where T : class, IHasChanges
{
if (subject is null)
{
throw new ArgumentNullException(nameof(subject));
}
if (changeTracker is null)
{
throw new ArgumentNullException(nameof(changeTracker));
}
changeTracker.Add(subject);
return subject;
}
public static T TrackChanges<T>(this T subject, ITracksChanges trackerOwner)
where T : class, IHasChanges
{
if (subject is null)
{
throw new ArgumentNullException(nameof(subject));
}
if (trackerOwner is null)
{
throw new ArgumentNullException(nameof(trackerOwner));
}
return TrackChanges(subject, trackerOwner.ChangeTracker);
}
}
}
using System;
using System.Diagnostics;
using System.Reactive;
namespace PropertyFacadeExample.Domain
{
public interface ICachedObservable<T> : IReadOnlyValueObservable<T>
{
bool HasValue { get; }
}
/// <summary>
/// Represents an observable that caches the latest value of another observable.
/// This observable has the same subscription semantics as ReplaySubject(1): if there is a value present, the value will replay when a subscription is made.
/// </summary>
/// <typeparam name="T"></typeparam>
[DebuggerDisplay("(CachedObservable) {Value}")]
public sealed class CachedObservable<T> : ObservableBase<T>, ICachedObservable<T>
{
private readonly IDisposable _registration;
private readonly IObservable<T> _source;
public bool HasValue { get; private set; }
public T Value { get; private set; }
public CachedObservable(IObservable<T> source)
{
_source = source ?? throw new ArgumentNullException(nameof(source));
_registration = _source.Subscribe(x =>
{
HasValue = true;
Value = x;
});
}
public void Dispose() => _registration.Dispose();
protected override IDisposable SubscribeCore(IObserver<T> observer)
{
if (observer is null)
{
throw new ArgumentNullException(nameof(observer));
}
var sub = _source.SubscribeSafe(observer);
if (HasValue)
{
observer.OnNext(Value);
}
return sub;
}
public override string ToString() => $"(CachedObservable) {Value}";
}
public static class CachedObservableExtensions
{
public static ICachedObservable<T> ToCached<T>(this IObservable<T> source) => new CachedObservable<T>(source);
}
}
using System;
namespace PropertyFacadeExample.Domain
{
internal static class RxDomain
{
public static IValueObservable<T> ObservableProperty<T>()
=> new ValueObservable<T>();
public static IValueObservable<T> ObservableProperty<T>(T defaultValue, CoerceValueHandler<T> coerceValue = null)
=> new ValueObservable<T>(defaultValue, coerceValue);
public static IValueObservable<T> ObservableProperty<T>(T defaultValue, CoerceValueHandler<T> coerceValue, out IDisposable coercerDelayToken)
{
var obs = new ValueObservable<T>(defaultValue, coerceValue, delayCoercer: true);
coercerDelayToken = obs.GetCoercerDelayToken();
return obs;
}
public static IValueObservable<T> ObservableProperty<T>(CoerceValueHandler<T> coerceValue)
=> new ValueObservable<T>(default, coerceValue);
public static IReadOnlyValueObservable<T> ReadOnlyConstant<T>(T value)
=> new ReadOnlyConstantValueObservable<T>(value);
}
}
using System;
using System.Diagnostics;
using System.Reactive.Disposables;
using System.Reactive.Subjects;
namespace PropertyFacadeExample.Domain
{
public interface IReadOnlyValueObservable<T> : IObservable<T>, IDisposable
{
T Value { get; }
}
public interface IValueObservable<T> : IReadOnlyValueObservable<T>, ISubject<T>
{
new T Value { get; set; }
}
/// <summary>
/// Same thing as Observable.Return() except as a <see cref="IReadOnlyValueObservable{T}"/>.
/// </summary>
/// <typeparam name="T"></typeparam>
public sealed class ReadOnlyConstantValueObservable<T> : IReadOnlyValueObservable<T>
{
public T Value { get; }
public ReadOnlyConstantValueObservable(T value) => Value = value;
public void Dispose()
{ }
public IDisposable Subscribe(IObserver<T> observer)
{
if (observer is null)
{
throw new ArgumentNullException(nameof(observer));
}
observer.OnNext(Value);
return Disposable.Empty;
}
}
public delegate T CoerceValueHandler<T>(T oldValue, T newValue);
/// <summary>
/// Represents a reactive subject that caches the latest value. It exhibits the same behavior as
/// <see cref="BehaviorSubject{T}"/> except it has a settable <see cref="Value"/> property.
/// </summary>
/// <typeparam name="T"></typeparam>
[DebuggerDisplay("(ValueObservable) {Value}")]
public sealed class ValueObservable<T> : IValueObservable<T>
{
private readonly BehaviorSubject<T> _subject;
private readonly CoerceValueHandler<T> _coerceValue;
public ValueObservable(T initialValue = default, CoerceValueHandler<T> coerceValue = default, bool delayCoercer = false)
{
_coerceValue = coerceValue ?? ((_, newValue) => newValue);
_subject = new BehaviorSubject<T>(delayCoercer ? initialValue : _coerceValue.Invoke(default, initialValue));
}
public T Value
{
get => _subject.Value;
set => OnNext(value);
}
public IDisposable Subscribe(IObserver<T> observer) => _subject.SubscribeSafe(observer);
public void OnCompleted() => _subject.OnCompleted();
public void OnError(Exception error) => _subject.OnError(error);
public void OnNext(T newValue) => _subject.OnNext(_coerceValue.Invoke(Value, newValue));
public void Dispose() => _subject.Dispose();
public override string ToString() => $"(ValueObservable<{typeof(T).Name}>) {Value}";
public void CoerceCurrent() => _coerceValue.Invoke(Value, Value);
public IDisposable GetCoercerDelayToken() => Disposable.Create(() => CoerceCurrent());
}
/// <summary>
/// Allows you to build custom observer logic for complex coercion scenarios.
/// </summary>
/// <typeparam name="T"></typeparam>
public sealed class ValueObservableFacade<T> : IValueObservable<T>
{
private readonly IObservable<T> _observable;
private readonly IObserver<T> _observer;
private readonly Func<T> _getValue;
private readonly Action<T> _setValue;
public ValueObservableFacade(IObservable<T> observable, IObserver<T> observer, Func<T> getValue, Action<T> setValue = null)
{
_observable = observable ?? throw new ArgumentNullException(nameof(observable));
_observer = observer ?? throw new ArgumentNullException(nameof(observer));
_getValue = getValue ?? throw new ArgumentNullException(nameof(getValue));
_setValue = setValue;
_observable.Subscribe(next => Value = next);
}
/// <summary>
/// Gets or sets the value. If the value is the same as the one stored, observers will not be notified. Use <see cref="OnNext(T)"/> to override this.
/// </summary>
public T Value
{
get => _getValue();
set
{
if (!Equals(value, _getValue()))
{
OnNext(value);
}
}
}
public void Dispose()
{
if (_observer is IDisposable d)
{
d.Dispose();
}
}
public void OnCompleted() => _observer.OnCompleted();
public void OnError(Exception error) => _observer.OnError(error);
public void OnNext(T value)
{
_setValue?.Invoke(value);
_observer.OnNext(value);
}
public IDisposable Subscribe(IObserver<T> observer) => _observable.SubscribeSafe(observer);
}
}
using System.Linq;
using System.Reactive.Linq;
using static PropertyFacadeExample.Domain.RxDomain;
namespace PropertyFacadeExample.Domain.Models
{
/// <summary>
/// An example domain model that encapsulates working time in hours.
/// </summary>
public sealed class WorkingAttendance
{
public IValueObservable<decimal> AdminATime { get; }
public IValueObservable<decimal> AdminBTime { get; }
public IValueObservable<decimal> NonSalaryTime { get; }
public IValueObservable<decimal> SalaryTime { get; }
public IValueObservable<decimal> TravelTime { get; }
public IValueObservable<decimal> LeaveTime { get; }
public IReadOnlyValueObservable<decimal> TotalTime { get; }
public WorkingAttendance(
decimal administrativeATime,
decimal administrativeBTime,
decimal nonSalaryTime,
decimal salaryTime,
decimal travelTime,
decimal leaveTime)
{
AdminATime = ObservableProperty(administrativeATime, ClipNegativeHoursToZero);
AdminBTime = ObservableProperty(administrativeBTime, ClipNegativeHoursToZero);
NonSalaryTime = ObservableProperty(nonSalaryTime);
SalaryTime = ObservableProperty(salaryTime);
TravelTime = ObservableProperty(travelTime);
LeaveTime = ObservableProperty(leaveTime, ClipNegativeHoursToZero);
TotalTime = Observable.CombineLatest(
AdminATime,
AdminBTime,
NonSalaryTime,
SalaryTime,
TravelTime,
LeaveTime)
.Select(hours => hours.Sum())
.ToCached();
}
private static decimal ClipNegativeHoursToZero(decimal _, decimal newTime) => newTime < 0 ? 0 : newTime;
}
}