Skip to content

Instantly share code, notes, and snippets.

@Logopher
Last active October 1, 2022 19:34
Show Gist options
  • Select an option

  • Save Logopher/6fa9f33d1eef5348602a93a14fb7cfba to your computer and use it in GitHub Desktop.

Select an option

Save Logopher/6fa9f33d1eef5348602a93a14fb7cfba to your computer and use it in GitHub Desktop.
An MVVM observable implementation intended to reduce repetitive code.
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