using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Serialization; //using PropertyChanged; namespace InABox.Core { public class DoNotSerialize : Attribute { } public interface IBaseObject { public bool IsChanged(); public void CancelChanges(); public void CommitChanges(); public bool IsObserving(); public void SetObserving(bool active); } /// /// Observable object with INotifyPropertyChanged implemented /// public abstract class BaseObject : INotifyPropertyChanged, IBaseObject { public BaseObject() { SetObserving(false); DatabaseSchema.InitializeSubObjects(this); Init(); SetObserving(true); } [OnDeserializing] internal void OnDeserializingMethod(StreamingContext context) { if (_observing) SetObserving(false); } [OnDeserialized] internal void OnDeserializedMethod(StreamingContext context) { if (!_observing) SetObserving(true); } protected virtual void Init() { CheckOriginalValues(); LoadedColumns = new HashSet(); UserProperties = new UserProperties(); //UserProperties.ParentType = this.GetType(); DatabaseSchema.InitializeObject(this); UserProperties.OnPropertyChanged += (o, n, b, a) => { if (IsObserving()) OnPropertyChanged(n, b, a); }; CheckSequence(); } private void CheckSequence() { if (this is ISequenceable seq && seq.Sequence <= 0) { seq.Sequence = CoreUtils.GenerateSequence(); } } #region Observing Flags public static bool GlobalObserving = true; private bool _observing = true; public bool IsObserving() { return GlobalObserving && _observing; } public void SetObserving(bool active) { bApplyingChanges = true; _observing = active; foreach (var oo in DatabaseSchema.GetSubObjects(this)) oo.SetObserving(active); bApplyingChanges = false; } protected virtual void DoPropertyChanged(string name, object? before, object? after) { } public event PropertyChangedEventHandler? PropertyChanged; private bool bApplyingChanges; private bool bChanged; [DoNotPersist] public ConcurrentDictionary OriginalValues { get; set; } [DoNotPersist] [DoNotSerialize] public HashSet LoadedColumns { get; set; } protected virtual void SetChanged(string name, object? before, object? after) { bChanged = true; if (!bApplyingChanges) { try { bApplyingChanges = true; DoPropertyChanged(name, before, after); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } catch (Exception e) { } bApplyingChanges = false; } } private bool QueryChanged() { CheckOriginalValues(); if (OriginalValues.Count > 0) return true; foreach (var oo in DatabaseSchema.GetSubObjects(this)) if (oo.IsChanged()) return true; return false; } public void OnPropertyChanged(string name, object? before, object? after) { if (!IsObserving()) return; if (name.Equals("IsChanged")) return; if (name.Equals("Observing")) return; if (name.Equals("OriginalValues")) return; LoadedColumns.Add(name); if (!BaseObjectExtensions.HasChanged(before, after)) return; CheckOriginalValues(); if (!OriginalValues.ContainsKey(name)) OriginalValues[name] = before; SetChanged(name, before, after); } private void CheckOriginalValues() { if (OriginalValues == null) { var bObserving = _observing; _observing = false; OriginalValues = new ConcurrentDictionary(); _observing = bObserving; } } public bool IsChanged() { return IsObserving() ? QueryChanged() : bChanged; } public void CancelChanges() { bApplyingChanges = true; var bObs = IsObserving(); SetObserving(false); CheckOriginalValues(); foreach (var key in OriginalValues.Keys.ToArray()) { try { var prop = key.Contains(".") ? CoreUtils.GetProperty(GetType(),key) : GetType().GetRuntimeProperty(key); if(prop != null) { if (prop.SetMethod != null) { var val = OriginalValues[key]; // Funky 64bit stuff here? if (prop.PropertyType == typeof(int) && val?.GetType() == typeof(long)) val = Convert.ToInt32(val); prop.SetValue(this, val); } } else if (UserProperties.ContainsKey(key)) { UserProperties[key] = OriginalValues[key]; } else { Logger.Send(LogType.Error, "", $"'{key}' is neither a runtime property nor custom property of {GetType().Name}"); } } catch (Exception e) { Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace)); } } OriginalValues.Clear(); bChanged = false; foreach (var oo in DatabaseSchema.GetSubObjects(this)) oo.CancelChanges(); SetObserving(bObs); bApplyingChanges = false; } public void CommitChanges() { bApplyingChanges = true; CheckOriginalValues(); OriginalValues.Clear(); bChanged = false; foreach (var oo in DatabaseSchema.GetSubObjects(this)) oo.CommitChanges(); bApplyingChanges = false; } public string ChangedValues() { var result = new List(); var type = GetType(); try { CheckOriginalValues(); foreach (var key in OriginalValues.Keys) try { if (UserProperties.ContainsKey(key)) { var obj = UserProperties[key]; result.Add(string.Format("[{0} = {1}]", key, obj != null ? obj.ToString() : "null")); } else { var prop = DatabaseSchema.Property(type, key);// GetType().GetProperty(key); if (prop is StandardProperty standard && standard.Loggable != null) { /*var attribute = //prop.GetCustomAttributes(typeof(LoggablePropertyAttribute), true).FirstOrDefault(); if (attribute != null) {*/ //var lpa = (LoggablePropertyAttribute)attribute; var format = standard.Loggable.Format; var value = standard.Getter()(this); if (string.IsNullOrEmpty(format)) result.Add($"[{key} = {value}]"); else result.Add(string.Format("[{0} = {1:" + format + "}]", key, value)); //} } } } catch (Exception e) { Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace)); } } catch (Exception e) { Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace)); } return string.Join(" ", result); } #endregion #region UserProperties private UserProperties _userproperties; [DoNotPersist] public UserProperties UserProperties { get { CheckUserProperties(); return _userproperties; } set { _userproperties = value; CheckUserProperties(); } } private static Dictionary? DefaultProperties; private void CheckUserProperties() { if (_userproperties == null) { _userproperties = new UserProperties(); if (DefaultProperties == null) { DefaultProperties = new Dictionary(); var props = DatabaseSchema.Properties(GetType()).Where(x => x is CustomProperty).ToArray(); foreach (var field in props) DefaultProperties[field.Name] = DatabaseSchema.DefaultValue(field.PropertyType); } _userproperties.LoadFromDictionary(DefaultProperties); } } #endregion } public class BaseObjectSnapshot where T : BaseObject { private readonly List<(IProperty, object?)> Values; private readonly T Object; public BaseObjectSnapshot(T obj) { Values = new List<(IProperty, object?)>(); foreach(var property in DatabaseSchema.Properties(obj.GetType())) { Values.Add((property, property.Getter()(obj))); } Object = obj; } public void ResetObject() { Object.CancelChanges(); var bObs = Object.IsObserving(); Object.SetObserving(false); foreach(var (prop, value) in Values) { var oldValue = prop.Getter()(Object); prop.Setter()(Object, value); if(BaseObjectExtensions.HasChanged(oldValue, value)) { var parent = prop.Parent != null ? prop.Parent.Getter()(Object) as BaseObject : Object; if(parent != null) { var localPropName = prop is StandardProperty stdProp ? stdProp.Property.Name : prop.Name; parent.OriginalValues[localPropName] = oldValue; } } } Object.SetObserving(bObs); } } public static class BaseObjectExtensions { public static bool HasChanged(object? before, object? after) { if ((before == null || before.Equals("")) && (after == null || after.Equals(""))) return false; if (before == null != (after == null)) return true; if (!before!.GetType().Equals(after!.GetType())) return true; if (before is string[] && after is string[]) return !(before as string[]).SequenceEqual(after as string[]); return !before.Equals(after); } public static bool HasColumn(this T sender, string column) where T : BaseObject { return sender.LoadedColumns.Contains(column); } public static bool HasColumn(this T sender, Expression> column) where T : BaseObject { return sender.LoadedColumns.Contains(CoreUtils.GetFullPropertyName(column, ".")); } public static bool HasOriginalValue(this T sender, string propertyname) where T : BaseObject { return sender.OriginalValues != null && sender.OriginalValues.ContainsKey(propertyname); } public static TType GetOriginalValue(this T sender, string propertyname) where T : BaseObject { return sender.OriginalValues != null && sender.OriginalValues.ContainsKey(propertyname) ? (TType)CoreUtils.ChangeType(sender.OriginalValues[propertyname], typeof(TType)) : default; } /// /// Get all database values (i.e., non-calculated, local properties) for a given object . /// If is , only retrieve values which have changed. /// public static Dictionary GetValues(this T sender, bool all) where T : BaseObject { var result = new Dictionary(); foreach(var property in DatabaseSchema.Properties(sender.GetType())) { if (property.IsDBColumn) { var isLocal = !property.HasParentEntityLink() || (property.Parent?.HasParentEntityLink() != true && property.Name.EndsWith(".ID")); if (isLocal) { if (all) { result[property.Name] = property.Getter()(sender); } else { if(property is StandardProperty stdProp) { var parent = property.Parent != null ? property.Parent.Getter()(sender) as BaseObject : sender; if(parent?.HasOriginalValue(stdProp.Property.Name) == true) { result[property.Name] = property.Getter()(sender); } } else if(property is CustomProperty customProp) { if (sender.HasOriginalValue(customProp.Name)) { result[property.Name] = property.Getter()(sender); } } } } } } return result; } public static Dictionary GetOriginaValues(this T sender) where T : BaseObject { var result = new Dictionary(); foreach(var property in DatabaseSchema.Properties(sender.GetType())) { if (property.IsDBColumn) { var isLocal = !property.HasParentEntityLink() || (property.Parent?.HasParentEntityLink() != true && property.Name.EndsWith(".ID")); if (isLocal) { var parent = property.Parent != null ? property.Parent.Getter()(sender) as BaseObject : sender; var localPropName = property is StandardProperty stdProp ? stdProp.Property.Name : property.Name; if (parent != null) if (parent.OriginalValues.TryGetValue(localPropName, out var value)) result[property.Name] = value; } } } return result; } public static BaseObjectSnapshot TakeSnapshot(this T obj) where T : BaseObject { return new BaseObjectSnapshot(obj); } public static List Compare(this T sender, Dictionary original) where T : BaseObject { var result = new List(); var current = GetValues(sender, true); foreach (var key in current.Keys) if (original.ContainsKey(key)) { if (current[key] == null) { if (original[key] != null) result.Add(string.Format("[{0}] has changed from [{1}] to [{2}]", key, original[key], current[key])); } else { if (!current[key].Equals(original[key])) result.Add(string.Format("[{0}] has changed from [{1}] to [{2}]", key, original[key], current[key])); } } else { result.Add(string.Format("[{0}] not present in previous dictionary!", key)); } return result; } public static bool GetValue(this T sender, Expression> property, bool original, out TType result) where T : BaseObject { if (sender.HasOriginalValue(property)) { if (original) result = sender.GetOriginalValue(property); else { var expr = property.Compile(); result = expr(sender); } return true; } result = default(TType); return false; } public static void SetOriginalValue(this T sender, string propertyname, TType value) where T : BaseObject { sender.OriginalValues[propertyname] = value; } public static bool HasOriginalValue(this T sender, Expression> property) where T : BaseObject { //var prop = ((MemberExpression)property.Body).Member as PropertyInfo; String propname = CoreUtils.GetFullPropertyName(property, "."); return !String.IsNullOrWhiteSpace(propname) && sender.OriginalValues != null && sender.OriginalValues.ContainsKey(propname); } public static TType GetOriginalValue(this T sender, Expression> property) where T : BaseObject { var prop = ((MemberExpression)property.Body).Member as PropertyInfo; return prop != null && sender.OriginalValues != null && sender.OriginalValues.ContainsKey(prop.Name) ? (TType)CoreUtils.ChangeType(sender.OriginalValues[prop.Name], typeof(TType)) : default; } public static TType GetOriginalValue(this T sender, Expression> property, TType defaultValue) where T : BaseObject { var prop = ((MemberExpression)property.Body).Member as PropertyInfo; return prop != null && sender.OriginalValues != null && sender.OriginalValues.ContainsKey(prop.Name) ? (TType)CoreUtils.ChangeType(sender.OriginalValues[prop.Name], typeof(TType)) : defaultValue; } public static void SetOriginalValue(this T sender, Expression> property, TType value) where T : BaseObject { var prop = ((MemberExpression)property.Body).Member as PropertyInfo; sender.OriginalValues[prop.Name] = value; } } /// /// An is loggable if it has the defined on it.
/// If it is part of an , then it is only loggable if the property on the parent class /// also has . ///
public class LoggablePropertyAttribute : Attribute { public string Format { get; set; } } }