mysimulation/server/tso.common/Utils/Binding.cs
Tony Bark 22191ce648 Removed NioTSO client and server
- NioTSO client isn't needed because we're using RayLib
- Added FreeSO's API server to handle most backend operations
2024-05-01 02:55:43 -04:00

301 lines
9.1 KiB
C#
Executable file

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
namespace FSO.Common.Utils
{
public class Binding <T> : IDisposable where T : INotifyPropertyChanged
{
private List<IBinding> Bindings = new List<IBinding>();
private HashSet<INotifyPropertyChanged> Watching = new HashSet<INotifyPropertyChanged>();
private T _Value;
public event Callback<T> ValueChanged;
public Binding()
{
lock(Binding.All)
Binding.All.Add(this);
}
~Binding()
{
//i don't think the garbage collector will remove this if there is still a reference to its callback function.
ClearWatching();
}
public void Dispose()
{
ClearWatching();
Bindings = null;
}
public T Value
{
get
{
return _Value;
}
set
{
ClearWatching();
_Value = value;
if(_Value != null){
Watch(_Value, null);
}
Digest();
if(ValueChanged != null)
{
ValueChanged(_Value);
}
}
}
private void ClearWatching()
{
lock (Watching)
{
var clone = new List<INotifyPropertyChanged>(Watching);
foreach (var item in clone)
{
item.PropertyChanged -= OnPropertyChanged;
Watching.Remove(item);
}
}
}
private void Watch(INotifyPropertyChanged source, HashSet<INotifyPropertyChanged> toWatch)
{
lock (Watching)
{
if (!Watching.Contains(source))
{
source.PropertyChanged += OnPropertyChanged;
Watching.Add(source);
}
toWatch?.Add(source);
var properties = source.GetType().GetProperties();
foreach (var property in properties)
{
if (typeof(INotifyPropertyChanged).IsAssignableFrom(property.PropertyType))
{
var value = (INotifyPropertyChanged)property.GetValue(source, null);
if (value != null)
{
Watch(value, toWatch);
}
}
}
}
}
private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
lock (Watching)
{
var property = sender.GetType().GetProperty(e.PropertyName);
if (typeof(INotifyPropertyChanged).IsAssignableFrom(property.PropertyType))
{
//We may have a new nested object to watch
//ClearWatching();
if (_Value != null)
{
var toWatch = new HashSet<INotifyPropertyChanged>();
Watch(_Value, toWatch);
var toRemove = Watching.Except(toWatch).ToList();
foreach (var rem in toRemove)
{
Watching.Remove(rem);
rem.PropertyChanged -= OnPropertyChanged;
}
}
}
}
Digest();
}
//Using a dumb digest system for now, not very efficient but works
private void Digest()
{
GameThread.InUpdate(() => _Digest());
}
private void _Digest()
{
lock (this)
{
if (Bindings == null) return;
foreach (var binding in Bindings)
{
binding.Digest(_Value);
}
}
}
public Binding<T> WithBinding(object target, string targetProperty, string sourcePath)
{
//If top level changes, we need to update children
//If member of top level changes, we need to update children
var binding = new DotPathBinding(target, targetProperty, DotPath.CompileDotPath(typeof(T), sourcePath));
Bindings.Add(binding);
return this;
}
public Binding<T> WithBinding(object target, string targetProperty, string sourcePath, Func<object, object> valueConverter)
{
//If top level changes, we need to update children
//If member of top level changes, we need to update children
var binding = new DotPathBinding(target, targetProperty, DotPath.CompileDotPath(typeof(T), sourcePath), valueConverter);
Bindings.Add(binding);
return this;
}
public Binding<T> WithMultiBinding(Callback<BindingChange[]> callback, params string[] paths)
{
var compiledPaths = new PropertyInfo[paths.Length][];
for(int i=0; i < paths.Length; i++){
compiledPaths[i] = DotPath.CompileDotPath(typeof(T), paths[i]);
}
var binding = new MultiDotPathBinding(callback, paths, compiledPaths);
Bindings.Add(binding);
return this;
}
}
public class BindingChange
{
public string Path;
public object PreviousValue;
public object Value;
}
class MultiDotPathBinding : IBinding
{
private Callback<BindingChange[]> Callback;
private PropertyInfo[][] Paths;
private string[] PathStrings;
private object[] Values;
public MultiDotPathBinding(Callback<BindingChange[]> callback, string[] pathStrings, PropertyInfo[][] paths)
{
Callback = callback;
Paths = paths;
PathStrings = pathStrings;
Values = new object[Paths.Length];
}
public void Digest(object source)
{
List<BindingChange> changes = null;
for(int i=0; i < Paths.Length; i++){
var path = Paths[i];
var value = DotPath.GetDotPathValue(source, path);
if(value != Values[i]){
//Changed
if (changes == null) { changes = new List<BindingChange>(); }
changes.Add(new BindingChange()
{
Path = PathStrings[i],
PreviousValue = Values[i],
Value = value
});
Values[i] = value;
}
}
if(changes != null)
{
Callback(changes.ToArray());
}
}
}
class DotPathBinding : Binding
{
private PropertyInfo[] Path;
private object LastValue;
private Func<object, object> Converter;
public DotPathBinding(object target, string targetProperty, PropertyInfo[] path, Func<object, object> converter) : this(target, targetProperty, path)
{
this.Converter = converter;
}
public DotPathBinding(object target, string targetProperty, PropertyInfo[] path) : base(target, targetProperty)
{
this.Path = path;
}
public override void Digest(object source){
var value = GetValue(source);
if(value != LastValue){
LastValue = value;
if (Converter != null)
{
SetValue(Converter(value));
}
else
{
SetValue(value);
}
}
}
private object GetValue(object source)
{
return DotPath.GetDotPathValue(source, Path);
}
}
public abstract class Binding : IBinding
{
private object Target;
private string TargetProperty;
private PropertyInfo[] TargetPropertyPath;
public Binding(object target, string targetProperty)
{
Target = target;
TargetProperty = targetProperty;
TargetPropertyPath = DotPath.CompileDotPath(target.GetType(), targetProperty);
}
protected void SetValue(object value)
{
DotPath.SetDotPathValue(Target, TargetPropertyPath, value);
}
public abstract void Digest(object source);
public virtual void Dispose() { Target = null; }
public static List<IDisposable> All = new List<IDisposable>();
public static void DisposeAll()
{
//dispose of all bindings that are left over (city leave return)
lock (All)
{
foreach (var item in All)
{
item.Dispose();
}
All.Clear();
}
}
}
interface IBinding
{
void Digest(object source);
//object GetValue(object source);
//void SetValue(object value);
}
}