Last active
August 3, 2023 04:57
-
-
Save SteffenBlake/ace74a893d0b16c30a7eb2a42d6d9230 to your computer and use it in GitHub Desktop.
PropertyWatcherBase
This file contains 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 System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Linq.Expressions; | |
using System.Runtime.CompilerServices; | |
using System.Transactions; | |
namespace Assets.Scripts.Imports | |
{ | |
internal static class ExpressionExtensions | |
{ | |
/// <summary> | |
/// Serialized a Lambda Member Expression into a stringified form | |
/// E.g. "m => m.Foo.Bar" will become "Foo.Bar" | |
/// </summary> | |
public static string GetFullMemberName(this LambdaExpression memberSelector) | |
{ | |
var currentExpression = memberSelector.Body; | |
if (currentExpression is not MemberExpression memberExpression) | |
{ | |
throw new Exception("Member Expressions only!"); | |
} | |
var name = memberExpression.Member.Name; | |
while (memberExpression.Expression is MemberExpression next) | |
{ | |
memberExpression = next; | |
name = memberExpression.Member.Name + "." + name; | |
} | |
return name; | |
} | |
} | |
/// <summary> | |
/// Transaction for caching of Events, with baked in callback | |
/// Will throw an exception if disposed of before <see cref="Commit"/> has been called | |
/// </summary> | |
public class PropertyChangeTransaction : IDisposable | |
{ | |
public PropertyChangeTransaction(Action callback) | |
{ | |
_callback = callback; | |
} | |
private readonly Action _callback; | |
public bool Committed { get; private set; } | |
public void Commit() | |
{ | |
Committed = true; | |
_callback(); | |
} | |
public void Dispose() | |
{ | |
if (!Committed) | |
{ | |
throw new TransactionException("Attempted to dispose of an uncommitted Transaction"); | |
} | |
} | |
} | |
/// <summary> | |
/// Custom Delegate for Property Change Events, utilizing "in structs" for minimum memory usage and maximum performance | |
/// </summary> | |
public delegate void ReadonlyDelegateHandler<T>(in T propertyName); | |
/// <summary> | |
/// Base class for Property Watchers, inherit from this class and utilize Full Properties to | |
/// Leverage the <see cref="Mutate{T}"/> and <see cref="BindChild{T}"/> methods | |
/// </summary> | |
/// <typeparam name="TSelf"></typeparam> | |
public abstract class PropertyWatcherBase<TSelf> | |
where TSelf : PropertyWatcherBase<TSelf> | |
{ | |
/// <summary> | |
/// Lower level Event for when a Property has been changed. | |
/// Best practice to utilize <see cref="BindTo"/> instead | |
/// </summary> | |
private event ReadonlyDelegateHandler<string> PropertyChanged; | |
private bool _inTransaction; | |
/// <summary> | |
/// Opens a Disposable Transaction for caching of Property Change Events | |
/// Utilize <see cref="PropertyChangeTransaction.Commit"/> to finalize the transaction | |
/// And trigger all distinct events at once. | |
/// </summary> | |
public PropertyChangeTransaction OpenTransaction() | |
{ | |
if (_inTransaction) | |
{ | |
throw new TransactionException("Attempted to open a nested transaction"); | |
} | |
_inTransaction = true; | |
return new PropertyChangeTransaction(CloseTransaction); | |
} | |
private readonly HashSet<string> _changedProperties = new (); | |
private void CloseTransaction() | |
{ | |
_inTransaction = false; | |
// Copy out events in case downstream event needs to open a transaction | |
var cachedProperties = _changedProperties.ToList(); | |
_changedProperties.Clear(); | |
foreach (var prop in cachedProperties) | |
{ | |
PropertyChanged?.Invoke(prop); | |
} | |
} | |
protected void BindChild<T>(ref T target, T value, [CallerMemberName] string name = null) | |
where T : PropertyWatcherBase<T> | |
{ | |
if (name == null) | |
{ | |
throw new ArgumentException(nameof(name)); | |
} | |
target = value; | |
target.PropertyChanged += (in string propertyName) => | |
{ | |
var prop = name; | |
var full = $"{name}.{propertyName}"; | |
if (_inTransaction) | |
{ | |
_changedProperties.Add(prop); | |
_changedProperties.Add(full); | |
} | |
else | |
{ | |
PropertyChanged?.Invoke(prop); | |
PropertyChanged?.Invoke(full); | |
} | |
}; | |
} | |
protected void Mutate<T>(ref T target, T value, [CallerMemberName] string name = null) | |
{ | |
if (name == null) | |
{ | |
throw new ArgumentException(nameof(name)); | |
} | |
if (target?.Equals(value) ?? value == null) | |
{ | |
return; | |
} | |
target = value; | |
if (_inTransaction) | |
{ | |
_changedProperties.Add(name); | |
} | |
else | |
{ | |
PropertyChanged?.Invoke(name); | |
} | |
} | |
public void BindTo(Action method) | |
{ | |
PropertyChanged += (in string _) => | |
{ | |
method(); | |
}; | |
} | |
public void BindTo<T>(Expression<Func<TSelf, T>> selector, Action method) | |
{ | |
BindTo(selector, (in T _) => method()); | |
} | |
public void BindTo<T>(Expression<Func<TSelf, T>> selector, ReadonlyDelegateHandler<T> method) | |
{ | |
var fmn = selector.GetFullMemberName(); | |
var compiled = selector.Compile(); | |
var concrete = (TSelf)this; | |
PropertyChanged += (in string prop) => | |
{ | |
if (prop != fmn) | |
{ | |
return; | |
} | |
method(compiled(concrete)); | |
}; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example Implementation:
Example Subscription to Events:
Example Mutation (Yep, its that easy!):
Example Transaction:
Notes
in
variables reduce memory allocation for high performance!