Last active
October 1, 2022 19:34
-
-
Save Logopher/6fa9f33d1eef5348602a93a14fb7cfba to your computer and use it in GitHub Desktop.
An MVVM observable implementation intended to reduce repetitive code.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using NLog; | |
| using System; | |
| using System.Collections.Generic; | |
| using System.ComponentModel; | |
| using System.Diagnostics; | |
| using System.Linq; | |
| using System.Linq.Expressions; | |
| using System.Reflection; | |
| using System.Runtime.CompilerServices; | |
| using System.Threading; | |
| namespace Observables | |
| { | |
| public class ObservableContext : INotifyPropertyChanged, INotifyPropertyChanging | |
| { | |
| static readonly Logger CurrentLogger = LogManager.GetCurrentClassLogger(); | |
| internal static readonly Thread HomeThread = Thread.CurrentThread; | |
| public ObservableContext() | |
| { | |
| if (Thread.CurrentThread != HomeThread) | |
| { | |
| CurrentLogger.Error("ObservableContext instances have been initialized in different threads. This is not OK."); | |
| } | |
| var type = GetType(); | |
| foreach (var property in type.GetProperties(BindingFlags.Instance) | |
| .Where(p => | |
| { | |
| var pType = p.PropertyType; | |
| return pType.IsConstructedGenericType && typeof(Observable).IsAssignableFrom(pType); | |
| })) | |
| { | |
| var obsv = property.GetValue(this) as Observable; | |
| obsv.Init(this, property.Name); | |
| } | |
| } | |
| protected static Observable<TValue> Register<TContext, TValue>( | |
| TValue initialValue = default!) | |
| where TContext : ObservableContext | |
| { | |
| return new SimpleObservable<TValue>(initialValue); | |
| } | |
| protected static Observable<TValue> Register<TContext, TValue>( | |
| Expression<Func<TContext, TValue>> get) | |
| where TContext : ObservableContext | |
| { | |
| var getter = get.Compile(); | |
| return new ComputedObservable<TValue>(ctx => getter((TContext)ctx)); | |
| } | |
| protected static Observable<TValue> Register<TContext, TValue>( | |
| Expression<Func<TContext, TValue>> get, | |
| Action<TContext, TValue> set) | |
| where TContext : ObservableContext | |
| { | |
| var getter = get.Compile(); | |
| return new TwoWayObservable<TValue>(ctx => getter((TContext)ctx), (ctx, value) => set((TContext)ctx, value)); | |
| } | |
| #region PropertyChanged | |
| public event PropertyChangedEventHandler? PropertyChanged; | |
| protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) | |
| { | |
| if (e == null) | |
| { | |
| throw new ArgumentNullException(nameof(e)); | |
| } | |
| PropertyChanged?.Invoke(this, e); | |
| } | |
| protected internal void OnPropertyChanged([CallerMemberName] string? propertyName = null) | |
| { | |
| OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); | |
| } | |
| #endregion | |
| #region PropertyChanging | |
| public event PropertyChangingEventHandler? PropertyChanging; | |
| protected virtual void OnPropertyChanging(PropertyChangingEventArgs e) | |
| { | |
| if (e == null) | |
| { | |
| throw new ArgumentNullException(nameof(e)); | |
| } | |
| PropertyChanging?.Invoke(this, e); | |
| } | |
| protected internal void OnPropertyChanging([CallerMemberName] string? propertyName = null) | |
| { | |
| OnPropertyChanging(new PropertyChangingEventArgs(propertyName)); | |
| } | |
| #endregion | |
| } | |
| public abstract class Observable | |
| { | |
| protected static readonly Stack<Observable> Stack = new(); | |
| readonly Dictionary<ObservableContext, Dictionary<string, Action>> _propertyChangedMap = new(); | |
| private string? _propertyName; | |
| public string PropertyName => _propertyName ?? throw new InvalidOperationException(); | |
| private ObservableContext? _context; | |
| internal ObservableContext Context => _context ?? throw new InvalidOperationException(); | |
| internal Observable() | |
| { | |
| } | |
| internal virtual void Init(ObservableContext context, string propertyName) | |
| { | |
| if (_context != null || _propertyName != null) | |
| { | |
| throw new InvalidOperationException(); | |
| } | |
| _context = context; | |
| _propertyName = propertyName; | |
| } | |
| internal void Subscribe(Observable obsv) | |
| { | |
| if (Context == null || obsv.Context == null) | |
| { | |
| throw new InvalidOperationException(); | |
| } | |
| var map = _propertyChangedMap[obsv.Context]; | |
| if (map.TryGetValue(obsv.PropertyName, out var _)) | |
| { | |
| throw new InvalidOperationException(); | |
| } | |
| map[obsv.PropertyName] = Update; | |
| } | |
| protected abstract void Update(); | |
| } | |
| public abstract class Observable<TValue> : Observable | |
| { | |
| public abstract TValue Value { get; set; } | |
| internal Observable() | |
| { | |
| } | |
| internal override void Init(ObservableContext context, string propertyName) | |
| { | |
| base.Init(context, propertyName); | |
| // Get the value to ensure it is available and cached, | |
| // and that all observables in the chain are properly subscribed to each other. | |
| Convert.ToString(Value); | |
| } | |
| public static implicit operator TValue(Observable<TValue> o) => o.Value; | |
| } | |
| internal sealed class SimpleObservable<TValue> : Observable<TValue> | |
| { | |
| private TValue _value; | |
| public override TValue Value | |
| { | |
| get | |
| { | |
| Stack.LastOrDefault()?.Subscribe(this); | |
| return _value; | |
| } | |
| set | |
| { | |
| _value = value; | |
| Context.OnPropertyChanged(PropertyName); | |
| } | |
| } | |
| public SimpleObservable(TValue initialValue) | |
| { | |
| _value = initialValue; | |
| } | |
| protected override void Update() | |
| { | |
| throw new NotImplementedException(); | |
| } | |
| } | |
| internal sealed class ComputedObservable<TValue> : Observable<TValue> | |
| { | |
| TValue? _cache; | |
| bool _isCacheValid = false; | |
| readonly Func<ObservableContext, TValue> _getter; | |
| public override TValue Value | |
| { | |
| get | |
| { | |
| Stack.LastOrDefault()?.Subscribe(this); | |
| if (!_isCacheValid) | |
| { | |
| Stack.Push(this); | |
| var temp = _getter(Context); | |
| if (!Equals(temp, _cache)) | |
| { | |
| Context.OnPropertyChanged(PropertyName); | |
| } | |
| Debug.Assert(this == Stack.Pop()); | |
| _cache = temp; | |
| _isCacheValid = true; | |
| } | |
| return _cache!; | |
| } | |
| set => throw new InvalidOperationException("This Observable is read-only."); | |
| } | |
| public ComputedObservable(Func<ObservableContext, TValue> get) | |
| { | |
| _getter = get; | |
| } | |
| protected override void Update() | |
| { | |
| _isCacheValid = false; | |
| Convert.ToString(Value); | |
| } | |
| } | |
| internal sealed class TwoWayObservable<TValue> : Observable<TValue> | |
| { | |
| TValue? _cache; | |
| bool _isCacheValid = false; | |
| readonly Func<ObservableContext, TValue> _getter; | |
| readonly Action<ObservableContext, TValue> _setter; | |
| public override TValue Value | |
| { | |
| get | |
| { | |
| Stack.LastOrDefault()?.Subscribe(this); | |
| if (!_isCacheValid) | |
| { | |
| Stack.Push(this); | |
| var temp = _getter(Context); | |
| if (!Equals(temp, _cache)) | |
| { | |
| Context.OnPropertyChanged(PropertyName); | |
| } | |
| Debug.Assert(this == Stack.Pop()); | |
| _cache = temp; | |
| _isCacheValid = true; | |
| } | |
| return _cache!; | |
| } | |
| set | |
| { | |
| _setter(Context, value); | |
| Context.OnPropertyChanged(PropertyName); | |
| _cache = value; | |
| _isCacheValid = true; | |
| } | |
| } | |
| public TwoWayObservable( | |
| Func<ObservableContext, TValue> get, | |
| Action<ObservableContext, TValue> set) | |
| { | |
| _getter = get; | |
| _setter = set; | |
| } | |
| protected override void Update() | |
| { | |
| _isCacheValid = false; | |
| Convert.ToString(Value); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment