diff --git a/MonkeyLoader/Configuration/ConfigKeyChangedEvent.cs b/MonkeyLoader/Configuration/ConfigKeyChangedEvent.cs
index 612b379..00d5871 100644
--- a/MonkeyLoader/Configuration/ConfigKeyChangedEvent.cs
+++ b/MonkeyLoader/Configuration/ConfigKeyChangedEvent.cs
@@ -1,7 +1,7 @@
using System.Collections.Specialized;
-using System.ComponentModel;
using System;
using System.Diagnostics.CodeAnalysis;
+using MonkeyLoader.Meta;
namespace MonkeyLoader.Configuration
{
@@ -24,14 +24,8 @@ namespace MonkeyLoader.Configuration
/// Represents the data for the and events.
///
/// The type of the key's value.
- public sealed class ConfigKeyChangedEventArgs : IConfigKeyChangedEventArgs
+ public sealed class ConfigKeyChangedEventArgs : ValueChangedEventArgs, IConfigKeyChangedEventArgs
{
- ///
- public NotifyCollectionChangedEventArgs? ChangedCollection { get; }
-
- ///
- public string? ChangedProperty { get; }
-
///
public Config Config { get; }
@@ -45,14 +39,6 @@ public sealed class ConfigKeyChangedEventArgs : IConfigKeyChangedEventArgs
///
public bool HasValue { get; }
- ///
- [MemberNotNullWhen(true, nameof(ChangedCollection))]
- public bool IsChangedCollection => ChangedCollection is not null;
-
- ///
- [MemberNotNullWhen(true, nameof(ChangedProperty))]
- public bool IsChangedProperty => ChangedProperty is not null;
-
///
/// Gets the configuration item who's value changed.
///
@@ -63,22 +49,6 @@ public sealed class ConfigKeyChangedEventArgs : IConfigKeyChangedEventArgs
///
public string? Label { get; }
- ///
- /// Gets the new value of the .
- /// This can be the default value.
- ///
- public T? NewValue { get; }
-
- object? IConfigKeyChangedEventArgs.NewValue => NewValue;
-
- ///
- /// Gets the old value of the .
- /// This can be the default value.
- ///
- public T? OldValue { get; }
-
- object? IConfigKeyChangedEventArgs.OldValue => OldValue;
-
///
/// Creates a new event args instance for a changed config item.
///
@@ -94,41 +64,23 @@ public sealed class ConfigKeyChangedEventArgs : IConfigKeyChangedEventArgs
public ConfigKeyChangedEventArgs(Config config, IDefiningConfigKey key,
bool hadValue, T? oldValue, bool hasValue, T? newValue, string? label,
string? changedProperty, NotifyCollectionChangedEventArgs? changedCollection)
+ : base(oldValue, newValue, changedProperty, changedCollection)
{
Config = config;
Key = key;
- OldValue = oldValue;
HadValue = hadValue;
- NewValue = newValue;
HasValue = hasValue;
Label = label;
-
- ChangedProperty = changedProperty;
- ChangedCollection = changedCollection;
}
}
///
/// Represents a non-generic .
///
- public interface IConfigKeyChangedEventArgs
+ public interface IConfigKeyChangedEventArgs : IValueChangedEventArgs
{
- ///
- /// Gets the changed collection event arguments,
- /// if this configuration item's changed value originated
- /// from an event.
- ///
- public NotifyCollectionChangedEventArgs? ChangedCollection { get; }
-
- ///
- /// Gets the name of the property that changed,
- /// if this configuration item's changed value originated
- /// from an event.
- ///
- public string? ChangedProperty { get; }
-
///
/// Gets the in which the change occured.
///
@@ -153,26 +105,6 @@ public interface IConfigKeyChangedEventArgs
///
public bool HasValue { get; }
- ///
- /// Gets whether this configuration item's changed value originated
- /// from an event.
- ///
- ///
- /// true if ChangedCollection is not null; otherwise, false.
- ///
- [MemberNotNullWhen(true, nameof(ChangedCollection))]
- public bool IsChangedCollection { get; }
-
- ///
- /// Gets whether this configuration item's changed value originated
- /// from an event.
- ///
- ///
- /// true if ChangedProperty is not null; otherwise, false.
- ///
- [MemberNotNullWhen(true, nameof(ChangedProperty))]
- public bool IsChangedProperty { get; }
-
///
/// Gets the configuration item who's value changed.
///
@@ -182,17 +114,5 @@ public interface IConfigKeyChangedEventArgs
/// Gets a custom label that may be set by whoever changed the configuration.
///
public string? Label { get; }
-
- ///
- /// Gets the new value of the configuration item.
- /// This can be the default value.
- ///
- public object? NewValue { get; }
-
- ///
- /// Gets the old value of the configuration item.
- /// This can be the default value.
- ///
- public object? OldValue { get; }
}
}
\ No newline at end of file
diff --git a/MonkeyLoader/Configuration/DefiningConfigKey.cs b/MonkeyLoader/Configuration/DefiningConfigKey.cs
index 00d05bf..128e7e7 100644
--- a/MonkeyLoader/Configuration/DefiningConfigKey.cs
+++ b/MonkeyLoader/Configuration/DefiningConfigKey.cs
@@ -29,13 +29,16 @@ namespace MonkeyLoader.Configuration
/// The type of the config item's value.
public sealed class DefiningConfigKey : Entity>, IDefiningConfigKey
{
- private readonly bool _canAlwaysHaveChanges;
+ private static readonly Type _valueType = typeof(T);
+ private readonly bool _canAlwaysHaveChanges;
private readonly Lazy _fullId;
private ConfigSection? _configSection;
private bool _hasChanges;
private ConfigKeyChangedEventHandler? _untypedChanged;
+ private ValueChangedEventHandler? _untypedValueChanged;
private T? _value;
+ private ValueChangedEventHandler? _valueChanged;
///
public IConfigKey AsUntyped { get; }
@@ -117,7 +120,7 @@ public ConfigSection Section
IDefiningConfigKey IEntity>.Self => this;
///
- public Type ValueType { get; } = typeof(T);
+ public Type ValueType => _valueType;
///
/// Gets the logger of the config this item belongs to.
@@ -367,27 +370,34 @@ public bool Validate(T value)
/// The collection change arguments for the value.
private void OnChanged(bool hadValue, T? oldValue, string? eventLabel, string? changedProperty = null, NotifyCollectionChangedEventArgs? changedCollection = null)
{
- // Add notify changed integration
- if (hadValue)
+ var sameReferences = ReferenceEquals(oldValue, _value);
+
+ if (!sameReferences)
{
- if (oldValue is INotifyPropertyChanged oldPropertyChanged)
- oldPropertyChanged.PropertyChanged -= ValuePropertyChanged;
+ // Remove NotifyChanged integration from old value
+ if (hadValue)
+ {
+ if (oldValue is INotifyPropertyChanged oldPropertyChanged)
+ oldPropertyChanged.PropertyChanged -= ValuePropertyChanged;
- if (oldValue is INotifyCollectionChanged oldCollectionChanged)
- oldCollectionChanged.CollectionChanged -= ValueCollectionChanged;
- }
+ if (oldValue is INotifyCollectionChanged oldCollectionChanged)
+ oldCollectionChanged.CollectionChanged -= ValueCollectionChanged;
+ }
- if (HasValue)
- {
- if (_value is INotifyPropertyChanged newPropertyChanged)
- newPropertyChanged.PropertyChanged += ValuePropertyChanged;
+ // Add NotifyChanged integration to new value
+ if (HasValue)
+ {
+ if (_value is INotifyPropertyChanged newPropertyChanged)
+ newPropertyChanged.PropertyChanged += ValuePropertyChanged;
- if (_value is INotifyCollectionChanged newCollectionChanged)
- newCollectionChanged.CollectionChanged += ValueCollectionChanged;
+ if (_value is INotifyCollectionChanged newCollectionChanged)
+ newCollectionChanged.CollectionChanged += ValueCollectionChanged;
+ }
}
- // Don't fire event if value didn't change
- if (ReferenceEquals(oldValue, _value) || (oldValue is not null && _value is not null && _value.Equals(oldValue)))
+ // Don't fire event if it wasn't triggered by event and the value didn't change
+ if ((sameReferences && changedProperty is null && changedCollection is null)
+ || (oldValue is not null && _value is not null && _value.Equals(oldValue)))
return;
HasChanges = true;
@@ -411,6 +421,24 @@ private void OnChanged(bool hadValue, T? oldValue, string? eventLabel, string? c
Logger.Error(ex.LogFormat($"Some untyped {nameof(Changed)} event subscriber(s) of key [{Id}] threw an exception:"));
}
+ try
+ {
+ _valueChanged?.TryInvokeAll(this, eventArgs);
+ }
+ catch (AggregateException ex)
+ {
+ Logger.Error(ex.LogFormat($"Some typed {nameof(INotifyValueChanged)}.{nameof(Changed)} event subscriber(s) of key [{Id}] threw an exception:"));
+ }
+
+ try
+ {
+ _untypedValueChanged?.TryInvokeAll(this, eventArgs);
+ }
+ catch (AggregateException ex)
+ {
+ Logger.Error(ex.LogFormat($"Some untyped {nameof(INotifyValueChanged)}.{nameof(Changed)} event subscriber(s) of key [{Id}] threw an exception:"));
+ }
+
Section.OnItemChanged(eventArgs);
}
@@ -431,13 +459,25 @@ event ConfigKeyChangedEventHandler? IDefiningConfigKey.Changed
add => _untypedChanged += value;
remove => _untypedChanged -= value;
}
+
+ event ValueChangedEventHandler? INotifyValueChanged.Changed
+ {
+ add => _untypedValueChanged += value;
+ remove => _untypedValueChanged -= value;
+ }
+
+ event ValueChangedEventHandler? INotifyValueChanged.Changed
+ {
+ add => _valueChanged += value;
+ remove => _valueChanged -= value;
+ }
}
///
/// Defines the definition for a config item.
///
public interface IDefiningConfigKey : ITypedConfigKey, IEntity,
- INestedIdentifiable, IPrioritizable
+ INestedIdentifiable, IPrioritizable, INotifyValueChanged
{
///
/// Gets the config this item belongs to.
@@ -541,14 +581,15 @@ public interface IDefiningConfigKey : ITypedConfigKey, IEntity
/// Triggered when the internal value of this config item changes.
///
- public event ConfigKeyChangedEventHandler? Changed;
+ public new event ConfigKeyChangedEventHandler? Changed;
}
///
/// Defines the typed definition for a config item.
///
/// The type of the config item's value.
- public interface IDefiningConfigKey : IDefiningConfigKey, ITypedConfigKey, IEntity>
+ public interface IDefiningConfigKey : IDefiningConfigKey, ITypedConfigKey,
+ IEntity>, INotifyValueChanged
{
///
/// Gets this config item's set value, falling back to the computed default.
diff --git a/MonkeyLoader/Meta/NotifyValueChangedEvent.cs b/MonkeyLoader/Meta/NotifyValueChangedEvent.cs
new file mode 100644
index 0000000..d641543
--- /dev/null
+++ b/MonkeyLoader/Meta/NotifyValueChangedEvent.cs
@@ -0,0 +1,159 @@
+using MonkeyLoader.Configuration;
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
+
+namespace MonkeyLoader.Meta
+{
+ ///
+ /// The delegate that is called for an 's
+ /// changed event.
+ ///
+ /// The object that sent the event.
+ /// The event containing details about the change.
+ public delegate void ValueChangedEventHandler(object sender, IValueChangedEventArgs valueChangedEventArgs);
+
+ ///
+ /// The delegate that is called for an 's
+ /// changed event.
+ ///
+ /// The type of the key's value.
+ /// The object that sent the event.
+ /// The event containing details about the change.
+ public delegate void ValueChangedEventHandler(object sender, ValueChangedEventArgs valueChangedEventArgs);
+
+ ///
+ /// Defines the generic interface for objects that wrap another value
+ /// and notify others about changes to it.
+ ///
+ /// The type of the wrapped value.
+ public interface INotifyValueChanged : INotifyValueChanged
+ {
+ ///
+ /// Triggered when the internal value wrapped by this object changes.
+ ///
+ public new event ValueChangedEventHandler? Changed;
+ }
+
+ ///
+ /// Defines the non-generic interface for objects that wrap another value
+ /// and notify others about changes to it.
+ ///
+ public interface INotifyValueChanged
+ {
+ ///
+ /// Triggered when the internal value wrapped by this object changes.
+ ///
+ public event ValueChangedEventHandler? Changed;
+ }
+
+ ///
+ /// Represents a non-generic .
+ ///
+ public interface IValueChangedEventArgs
+ {
+ ///
+ /// Gets the changed collection event arguments,
+ /// if this configuration item's changed value originated
+ /// from an event.
+ ///
+ public NotifyCollectionChangedEventArgs? ChangedCollection { get; }
+
+ ///
+ /// Gets the name of the property that changed,
+ /// if this configuration item's changed value originated
+ /// from an event.
+ ///
+ public string? ChangedProperty { get; }
+
+ ///
+ /// Gets whether this configuration item's changed value originated
+ /// from an event.
+ ///
+ ///
+ /// true if ChangedCollection is not null; otherwise, false.
+ ///
+ [MemberNotNullWhen(true, nameof(ChangedCollection))]
+ public bool IsChangedCollection { get; }
+
+ ///
+ /// Gets whether this configuration item's changed value originated
+ /// from an event.
+ ///
+ ///
+ /// true if ChangedProperty is not null; otherwise, false.
+ ///
+ [MemberNotNullWhen(true, nameof(ChangedProperty))]
+ public bool IsChangedProperty { get; }
+
+ ///
+ /// Gets the new value of the configuration item.
+ /// This can be the default value.
+ ///
+ public object? NewValue { get; }
+
+ ///
+ /// Gets the old value of the configuration item.
+ /// This can be the default value.
+ ///
+ public object? OldValue { get; }
+ }
+
+ ///
+ /// Represents the data for the
+ /// and events.
+ ///
+ /// The type of the wrapped value.
+ public class ValueChangedEventArgs : IValueChangedEventArgs
+ {
+ ///
+ public NotifyCollectionChangedEventArgs? ChangedCollection { get; }
+
+ ///
+ public string? ChangedProperty { get; }
+
+ ///
+ [MemberNotNullWhen(true, nameof(ChangedCollection))]
+ public bool IsChangedCollection => ChangedCollection is not null;
+
+ ///
+ [MemberNotNullWhen(true, nameof(ChangedProperty))]
+ public bool IsChangedProperty => ChangedProperty is not null;
+
+ ///
+ /// Gets the new value of the .
+ /// This can be the default value.
+ ///
+ public T? NewValue { get; }
+
+ object? IValueChangedEventArgs.NewValue => NewValue;
+
+ ///
+ /// Gets the old value of the .
+ /// This can be the default value.
+ ///
+ public T? OldValue { get; }
+
+ object? IValueChangedEventArgs.OldValue => OldValue;
+
+ ///
+ /// Creates a new event args instance for a changed config item.
+ ///
+ /// The optional old value.
+ /// The optional new value.
+ /// The name of the changed property on the value.
+ /// The collection change arguments for the value.
+ public ValueChangedEventArgs(T? oldValue, T? newValue,
+ string? changedProperty, NotifyCollectionChangedEventArgs? changedCollection)
+ {
+ OldValue = oldValue;
+ NewValue = newValue;
+
+ ChangedProperty = changedProperty;
+ ChangedCollection = changedCollection;
+ }
+ }
+}
\ No newline at end of file
diff --git a/MonkeyLoader/MonkeyLoader.csproj b/MonkeyLoader/MonkeyLoader.csproj
index 7c6495b..c1df3f2 100644
--- a/MonkeyLoader/MonkeyLoader.csproj
+++ b/MonkeyLoader/MonkeyLoader.csproj
@@ -11,7 +11,7 @@
True
MonkeyLoader
Banane9
- 0.25.0-beta
+ 0.24.14-MonkeySync-beta
A convenience and extendability focused mod loader using NuGet packages.
README.md
LGPL-3.0-or-later
@@ -77,6 +77,10 @@
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/MonkeyLoader/Sync/IgnoreSyncValueAttribute.cs b/MonkeyLoader/Sync/IgnoreSyncValueAttribute.cs
new file mode 100644
index 0000000..8fd5cd0
--- /dev/null
+++ b/MonkeyLoader/Sync/IgnoreSyncValueAttribute.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace MonkeyLoader.Sync
+{
+ ///
+ /// Marks a field or property with a compatible type
+ /// on a class deriving from
+ /// to be excluded from the automatically detected fields and properties.
+ ///
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+ public sealed class IgnoreSyncValueAttribute : MonkeyLoaderAttribute
+ { }
+}
\ No newline at end of file
diff --git a/MonkeyLoader/Sync/MonkeySyncMethod.cs b/MonkeyLoader/Sync/MonkeySyncMethod.cs
new file mode 100644
index 0000000..56789a3
--- /dev/null
+++ b/MonkeyLoader/Sync/MonkeySyncMethod.cs
@@ -0,0 +1,99 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MonkeyLoader.Sync
+{
+ ///
+ /// The delegate that is represented by MonkeySync methods.
+ ///
+ public delegate void MonkeySyncAction();
+
+ ///
+ /// The delegate that is represented by MonkeySync methods.
+ ///
+ /// The value that triggered this method.
+ ///
+ public delegate void MonkeySyncFunc(T trigger);
+
+ ///
+ /// Defines the non-generic interface for s.
+ ///
+ ///
+ public interface ILinkedMonkeySyncMethod : ILinkedMonkeySyncValue
+ where TSyncObject : ILinkedMonkeySyncObject
+ {
+ }
+
+ ///
+ /// Defines the generic interface for linked s.
+ ///
+ /// The type of the link object used by the sync object that this sync value links to.
+ /// The type of the sync object that may contain this sync value.
+ /// The type of the Value that can trigger this sync method.
+ public interface ILinkedMonkeySyncMethod : ILinkedMonkeySyncMethod, ILinkedMonkeySyncValue
+ where TSyncObject : ILinkedMonkeySyncObject
+ {
+ ///
+ /// Gets the delegate that can be triggered by this sync method.
+ ///
+ public MonkeySyncFunc Function { get; }
+ }
+
+ ///
+ /// Defines the interface for not yet linked s.
+ ///
+ ///
+ public interface IUnlinkedMonkeySyncMethod : IUnlinkedMonkeySyncValue
+ where TSyncObject : ILinkedMonkeySyncObject
+ where TLinkedSyncValue : ILinkedMonkeySyncValue
+ { }
+
+ ///
+ /// Implements an abstract example for s.
+ ///
+ ///
+ /// This class is in all likelihood not useful to actually derive from.
+ /// It mainly serves as an example for how an implementation could look.
+ ///
+ ///
+ public abstract class MonkeySyncMethod : MonkeySyncValue,
+ IUnlinkedMonkeySyncMethod,
+ ILinkedMonkeySyncMethod
+ where TSyncObject : class, ILinkedMonkeySyncObject
+ where TSyncValue : MonkeySyncMethod
+ {
+ ///
+ public MonkeySyncFunc Function { get; }
+
+ ///
+ /// Creates a new sync method instance that wraps the given ,
+ /// changes of which can trigger the target .
+ ///
+ /// The delegate that can be triggered by this sync method.
+ /// The value to wrap.
+ protected MonkeySyncMethod(MonkeySyncFunc function, T value) : base(value)
+ {
+ Function = function;
+ }
+
+ ///
+ /// Creates a new sync method instance that wraps the given ,
+ /// changes of which can trigger the target .
+ ///
+ /// The delegate that can be triggered by this sync method.
+ /// The value to wrap.
+ protected MonkeySyncMethod(MonkeySyncAction action, T value)
+ : this(_ => action(), value)
+ { }
+
+ TSyncValue? IUnlinkedMonkeySyncValue.EstablishLinkFor(TSyncObject syncObject, string name, bool fromRemote)
+ => EstablishLinkFor(syncObject, name, fromRemote);
+
+ ///
+ /// Creates structures that make this method triggerable by others locally or in shared environments,
+ /// whether they have the mod implementing the method or not.
+ ///
+ public abstract void MakeInvocation();
+ }
+}
\ No newline at end of file
diff --git a/MonkeyLoader/Sync/MonkeySyncObject.cs b/MonkeyLoader/Sync/MonkeySyncObject.cs
new file mode 100644
index 0000000..76418b9
--- /dev/null
+++ b/MonkeyLoader/Sync/MonkeySyncObject.cs
@@ -0,0 +1,393 @@
+using EnumerableToolkit;
+using HarmonyLib;
+using MonkeyLoader.Meta;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+
+namespace MonkeyLoader.Sync
+{
+ ///
+ /// Represents the method that will create new instances
+ /// of sync objects linking via .
+ ///
+ /// The type of the link object used by the sync object.
+ /// The created but not yet linked sync object.
+ public delegate IUnlinkedMonkeySyncObject SyncObjectFactory()
+ where TLink : class;
+
+ ///
+ /// Defines the generic interface for MonkeySync objects that have been linked.
+ ///
+ /// The type of the link object used by the sync object.
+ public interface ILinkedMonkeySyncObject : IMonkeySyncObject
+ {
+ ///
+ public new TLink LinkObject { get; }
+ }
+
+ ///
+ /// Defines the non-generic interface for MonkeySync objects.
+ ///
+ public interface IMonkeySyncObject : INotifyPropertyChanged, IDisposable
+ {
+ ///
+ /// Gets whether this sync object has a link object.
+ ///
+ [MemberNotNullWhen(true, nameof(LinkObject))]
+ public bool HasLinkObject { get; }
+
+ ///
+ /// Gets whether this sync object has a valid link.
+ ///
+ [MemberNotNullWhen(true, nameof(LinkObject))]
+ public bool IsLinkValid { get; }
+
+ ///
+ /// Gets the link object used by this sync object.
+ ///
+ public object LinkObject { get; }
+ }
+
+ ///
+ /// Defines the generic interface for MonkeySync objects that are yet to be linked.
+ ///
+ ///
+ public interface IUnlinkedMonkeySyncObject : ILinkedMonkeySyncObject
+ where TLink : class
+ {
+ ///
+ /// Establishes this sync object's link with the given object.
+ /// If the link is successfully created, the now linked sync object will be
+ /// added
+ /// to the .
+ ///
+ ///
+ /// If the link fails or gets broken, a new instance has to be created.
+ ///
+ /// The link object to be used by this sync object.
+ /// Whether the link is being established from the remote side.
+ /// true if the established link is valid; otherwise, false.
+ public bool LinkWith(TLink linkObject, bool fromRemote = false);
+ }
+
+ ///
+ /// Implements the abstract base for MonkeySync objects.
+ ///
+ ///
+ /// Automatically detects any instance fields / properties containing
+ /// MonkeySync values or methods compatible with .
+ /// Property names take precedence over their backing fields.
+ /// Use the to mark fields / properties that should be ignored.
+ /// If a property is marked, its backing field will be ignored too.
+ ///
+ /// The concrete type of the MonkeySync object.
+ ///
+ /// The -derived interface
+ /// that the unlinked MonkeySync values of this object must implement.
+ ///
+ ///
+ /// The -derived interface
+ /// that the linked MonkeySync values of this object must implement.
+ ///
+ /// The type of the link object used by the sync object.
+ public abstract class MonkeySyncObject : IUnlinkedMonkeySyncObject
+ where TSyncObject : MonkeySyncObject
+ where TLinkedSyncValue : ILinkedMonkeySyncValue>
+ where TUnlinkedSyncValue : IUnlinkedMonkeySyncValue, TLinkedSyncValue>
+ where TLink : class
+ {
+ ///
+ /// The getters for the automatically detected instance fields / properties by their name.
+ ///
+ ///
+ /// Property names take precedence over their backing fields.
+ /// Use the to mark fields / properties that should be ignored.
+ /// If a property is marked, its backing field will be ignored too.
+ ///
+ protected static readonly Dictionary> syncValueAccessorsByName = new(StringComparer.Ordinal);
+
+ ///
+ /// The instances associated with this sync object.
+ ///
+ protected readonly HashSet syncValues = [];
+
+ private bool _disposedValue;
+
+ ///
+ [MemberNotNullWhen(true, nameof(LinkObject))]
+ public bool HasLinkObject => LinkObject is not null;
+
+ ///
+ [MemberNotNullWhen(true, nameof(LinkObject))]
+ public abstract bool IsLinkValid { get; }
+
+ ///
+ public TLink LinkObject { get; private set; } = null!;
+
+ object IMonkeySyncObject.LinkObject => LinkObject;
+
+ static MonkeySyncObject()
+ {
+ var syncValueType = typeof(TUnlinkedSyncValue);
+ var syncValueFields = typeof(TSyncObject).GetFields(AccessTools.all)
+ .Where(field => !field.IsStatic && syncValueType.IsAssignableFrom(field.FieldType) && field.GetCustomAttribute() is null);
+
+ var syncValueProperties = typeof(TSyncObject).GetProperties(AccessTools.all)
+ .Where(property => syncValueType.IsAssignableFrom(property.PropertyType) && (!(property.GetGetMethod()?.IsStatic ?? true)));
+
+ foreach (var field in syncValueFields)
+ syncValueAccessorsByName.Add(field.Name, (TSyncObject instance) => (TUnlinkedSyncValue)field.GetValue(instance));
+
+ foreach (var property in syncValueProperties)
+ {
+ var fieldName = $"<{property.Name}>";
+ var potentialField = syncValueAccessorsByName.FirstOrDefault(entry => entry.Key.Contains(fieldName));
+ var hasIgnoreAttribute = property.GetCustomAttribute() is null;
+
+ if (potentialField.Value is not null)
+ {
+ syncValueAccessorsByName.Remove(potentialField.Key);
+
+ if (!hasIgnoreAttribute)
+ syncValueAccessorsByName.Add(property.Name, potentialField.Value);
+
+ continue;
+ }
+
+ if (!hasIgnoreAttribute)
+ syncValueAccessorsByName.Add(property.Name, (TSyncObject instance) => (TUnlinkedSyncValue)property.GetValue(instance));
+ }
+ }
+
+ ///
+ /// Initializes a new instance of this MonkeySync object.
+ ///
+ ///
+ /// When is not the type being instantiated.
+ ///
+ protected MonkeySyncObject()
+ {
+ if (GetType() != typeof(TSyncObject))
+ throw new InvalidOperationException("TSyncObject must be the concrete Type being instantiated!");
+ }
+
+ ///
+ /// Ensures any unmanaged resources are disposed.
+ ///
+ ~MonkeySyncObject()
+ {
+ Dispose(false);
+ }
+
+ ///
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in the 'OnDisposing()' or 'OnFinalizing()' methods
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ public bool LinkWith(TLink linkObject, bool fromRemote = false)
+ {
+ if (HasLinkObject)
+ throw new InvalidOperationException("Can only assign a link object once!");
+
+ LinkObject = linkObject;
+
+ if (!EstablishLink(fromRemote))
+ return false;
+
+ MonkeySyncRegistry.RegisterLinkedSyncObject(this);
+ return true;
+ }
+
+ ///
+ /// By default: Sets up the event handlers
+ /// and calls EstablishLinkFor
+ /// for every instance field / property on .
+ ///
+ /// The detected fields / properties are stored in syncValueAccessorsByName.
+ ///
+ /// This method is called by LinkWith
+ /// after the LinkObject has been assigned.
+ /// It should ensure that a link object created from the remote side
+ /// is handled appropriately and without duplications as well.
+ ///
+ ///
+ ///
+ protected virtual bool EstablishLink(bool fromRemote)
+ {
+ var success = true;
+
+ foreach (var syncValueAccessor in syncValueAccessorsByName)
+ {
+ var syncValue = syncValueAccessor.Value((TSyncObject)this);
+
+ success &= EstablishLinkFor(syncValue, syncValueAccessor.Key, fromRemote);
+ }
+
+ return success;
+ }
+
+ ///
+ /// Creates a link for the given sync value of the given name.
+ ///
+ ///
+ /// By default: Adds the given sync value to the set of instances and calls
+ /// .EstablishLinkFor(…).
+ /// If successful, the handler is subscribed to
+ /// the event of the linked value as well.
+ ///
+ /// The sync value to link.
+ /// The name of the sync value to link.
+ /// Whether the link is being established from the remote side.
+ /// true if the link was successfully created; otherwise, false.
+ protected virtual bool EstablishLinkFor(TUnlinkedSyncValue unlinkedSyncValue, string propertyName, bool fromRemote)
+ {
+ var linkedValue = unlinkedSyncValue.EstablishLinkFor((TSyncObject)this, propertyName, fromRemote);
+
+ if (linkedValue is null)
+ return false;
+
+ syncValues.Add(linkedValue);
+ linkedValue.Changed += (sender, changedArgs)
+ => OnPropertyChanged(propertyName);
+
+ return true;
+ }
+
+ ///
+ /// Cleans up any managed resources as part of disposing.
+ ///
+ ///
+ /// By default: Disposes all instances
+ /// that were added to the set of instances,
+ /// and the LinkObject if it's .
+ ///
+ protected virtual void OnDisposing()
+ {
+ foreach (var syncValue in syncValues)
+ syncValue.Dispose();
+
+ if (LinkObject is IDisposable disposable)
+ disposable.Dispose();
+ }
+
+ ///
+ /// Cleans up any unmanaged resources as part of
+ /// disposing or finalization.
+ ///
+ protected virtual void OnFinalizing()
+ { }
+
+ ///
+ /// Tries to restore the link when it becomes invalidated
+ /// and triggers the Invalidated when that fails.
+ /// Afterwards, the object is automatically disposed.
+ ///
+ ///
+ /// Should be called from a derived class when something happens
+ /// that makes IsLinkValid false.
+ ///
+ protected void OnLinkInvalidated()
+ {
+ if (!IsLinkValid && TryRestoreLink() && IsLinkValid)
+ return;
+
+ try
+ {
+ Invalidated.TryInvokeAll();
+ }
+ catch { }
+
+ Dispose();
+ }
+
+ ///
+ /// Triggers the PropertyChanged
+ /// event with the given .
+ ///
+ ///
+ /// This is automatically called for any fields / properties.
+ ///
+ /// The name of the property that changed.
+ protected void OnPropertyChanged(string propertyName)
+ {
+ var eventData = new PropertyChangedEventArgs(propertyName);
+
+ PropertyChanged?.Invoke(this, eventData);
+ }
+
+ ///
+ /// By default: Calls TryRestoreLinkFor
+ /// for every in .
+ ///
+ /// This method is called by OnLinkInvalidated
+ /// if IsLinkValid has become false.
+ /// It should ensure that any still valid links are
+ /// handled appropriately and without duplications as well.
+ ///
+ ///
+ ///
+ protected virtual bool TryRestoreLink()
+ {
+ var success = true;
+
+ foreach (var syncValue in syncValues)
+ success &= TryRestoreLinkFor(syncValue);
+
+ return success;
+ }
+
+ ///
+ /// Tries to restore the link for the given sync value.
+ ///
+ ///
+ /// By default: Calls
+ /// .TryRestoreLink().
+ ///
+ /// The sync value to link.
+ /// true if the link was successfully restored; otherwise, false.
+ protected virtual bool TryRestoreLinkFor(TLinkedSyncValue syncValue)
+ => syncValue.TryRestoreLink();
+
+ private void Dispose(bool disposing)
+ {
+ if (_disposedValue)
+ return;
+
+ if (disposing)
+ OnDisposing();
+
+ OnFinalizing();
+
+ _disposedValue = true;
+ }
+
+ ///
+ /// Occurs when IsLinkValid becomes false
+ /// and it could not be restored.
+ ///
+ public event InvalidatedHandler? Invalidated;
+
+ ///
+ /// Represents the method that will handle the Invalidated
+ /// event raised when IsLinkValid becomes false
+ /// and it could not be restored.
+ ///
+ /// The sync object that got invalidated.
+ public delegate void InvalidatedHandler(TSyncObject syncObject);
+
+ ///
+ public event PropertyChangedEventHandler? PropertyChanged;
+ }
+}
\ No newline at end of file
diff --git a/MonkeyLoader/Sync/MonkeySyncObjectRegistration.cs b/MonkeyLoader/Sync/MonkeySyncObjectRegistration.cs
new file mode 100644
index 0000000..7c4b2ae
--- /dev/null
+++ b/MonkeyLoader/Sync/MonkeySyncObjectRegistration.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MonkeyLoader.Sync
+{
+ ///
+ /// A simple data class that allows the to track the known
+ /// MonkeySync object types.
+ ///
+ /// The type of the link object used by the sync object.
+ public sealed class MonkeySyncObjectRegistration
+ where TLink : class
+ {
+ private readonly SyncObjectFactory _createSyncObject;
+
+ ///
+ /// Gets the -unique name for the registered
+ /// MonkeySync object type.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Gets the of the registered
+ /// MonkeySync object.
+ ///
+ public Type SyncObjectType { get; }
+
+ ///
+ /// Creates a new instance of this data class with the given details for a
+ /// MonkeySync object type.
+ ///
+ /// The -unique name for the sync object type.
+ /// The type of the sync object.
+ /// A factory method that creates new instances of this sync object type.
+ public MonkeySyncObjectRegistration(string name, Type syncObjectType, SyncObjectFactory createSyncObject)
+ {
+ Name = name;
+ SyncObjectType = syncObjectType;
+ _createSyncObject = createSyncObject;
+ }
+
+ ///
+ /// Creates a new instance of the registered
+ /// MonkeySync object.
+ ///
+ /// The created but not yet linked sync object.
+ public IUnlinkedMonkeySyncObject CreateSyncObject()
+ => _createSyncObject();
+ }
+}
\ No newline at end of file
diff --git a/MonkeyLoader/Sync/MonkeySyncObjectRegistry.cs b/MonkeyLoader/Sync/MonkeySyncObjectRegistry.cs
new file mode 100644
index 0000000..832757e
--- /dev/null
+++ b/MonkeyLoader/Sync/MonkeySyncObjectRegistry.cs
@@ -0,0 +1,266 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+namespace MonkeyLoader.Sync
+{
+ ///
+ /// Handles globally registering MonkeySync objects for particular link types,
+ /// so that the link implementations can create and reference them.
+ ///
+ public static class MonkeySyncRegistry
+ {
+ ///
+ /// Gets the MonkeySync
+ /// object linked with the given link object.
+ ///
+ /// The found MonkeySync object.
+ /// When no sync object is registered for the link object.
+ ///
+ public static ILinkedMonkeySyncObject GetLinkedSyncObject(TLink linkObject)
+ where TLink : class
+ {
+ if (Library.TryGetLinkedSyncObject(linkObject, out var syncObject))
+ return syncObject;
+
+ throw new KeyNotFoundException("No sync object found for the given link object!");
+ }
+
+ ///
+ /// Gets the for the given .
+ ///
+ /// The type of registered sync object.
+ ///
+ public static MonkeySyncObjectRegistration GetSyncObjectRegistration(Type syncObjectType)
+ where TLink : class
+ => Library.GetSyncObjectRegistration(syncObjectType);
+
+ ///
+ /// Gets the for the given .
+ ///
+ /// The type of the link object used by the sync object.
+ /// The name of the registered sync object.
+ /// The found .
+ /// When no sync object with the given has been registered.
+ public static MonkeySyncObjectRegistration GetSyncObjectRegistration(string name)
+ where TLink : class
+ => Library.GetSyncObjectRegistration(name);
+
+ ///
+ /// Determines whether there is a registered MonkeySync
+ /// object that is linked to the given link object.
+ ///
+ ///
+ public static bool HasLinkedSyncObject(TLink linkObject)
+ where TLink : class
+ => Library.TryGetLinkedSyncObject(linkObject, out _);
+
+ ///
+ /// Registers a linked MonkeySync
+ /// object so that it can be accessed
+ /// through a reference to its link object.
+ ///
+ ///
+ /// The link is held in a -
+ /// as such, the sync object may be garbage collected when the link object is.
+ ///
+ /// The type of the link object used by the sync object.
+ /// The linked sync object to register.
+ public static void RegisterLinkedSyncObject(ILinkedMonkeySyncObject syncObject)
+ where TLink : class
+ => Library.RegisterLinkedSyncObject(syncObject);
+
+ ///
+ /// Registers a MonkeySync object type with the given details for it.
+ ///
+ /// The type of the link object used by the sync object.
+ /// The -unique name for the sync object type.
+ /// The type of the sync object.
+ /// A factory method that creates new instances of this sync object type.
+ /// The data of the newly registered sync object type.
+ ///
+ /// When the or one with the same
+ /// has already been registered.
+ ///
+ public static MonkeySyncObjectRegistration RegisterSyncObject(string name, Type syncObjectType, SyncObjectFactory createSyncObject)
+ where TLink : class
+ => Library.RegisterSyncObject(name, syncObjectType, createSyncObject);
+
+ ///
+ /// Tries to get the MonkeySync
+ /// object linked with the given link object.
+ ///
+ /// The type of the link object used by the sync object.
+ /// The link object used by the sync object.
+ /// The found MonkeySync object; otherwise, null.
+ /// true if a sync object is registered for the given link object; otherwise, false.
+ public static bool TryGetLinkedSyncObject(TLink linkObject, [NotNullWhen(true)] out ILinkedMonkeySyncObject? syncObject)
+ where TLink : class
+ => Library.TryGetLinkedSyncObject(linkObject, out syncObject);
+
+ ///
+ /// Tries to get the for the given .
+ ///
+ /// The type of the link object used by the sync object.
+ /// The name of the registered sync object.
+ /// The found ; otherwise, null.
+ /// true if the registered sync object type was found; otherwise, false.
+ public static bool TryGetSyncObjectRegistration(string name, [NotNullWhen(true)] out MonkeySyncObjectRegistration? registeredSyncObject)
+ where TLink : class
+ => Library.TryGetSyncObjectRegistration(name, out registeredSyncObject);
+
+ ///
+ /// Tries to get the for the given .
+ ///
+ /// The type of the sync object.
+ /// The found ; otherwise, null.
+ /// true if the registered sync object type was found; otherwise, false.
+ public static bool TryGetSyncObjectRegistration(Type syncObjectType, [NotNullWhen(true)] out MonkeySyncObjectRegistration? registeredSyncObject)
+ where TLink : class
+ => Library.TryGetSyncObjectRegistration(syncObjectType, out registeredSyncObject);
+
+ ///
+ /// Unregisters a linked MonkeySync
+ /// object so that it can't be accessed
+ /// through a reference to its link object anymore.
+ ///
+ /// The type of the link object used by the sync object.
+ /// The linked sync object to unregister.
+ public static bool UnregisterLinkedSyncObject(ILinkedMonkeySyncObject syncObject)
+ where TLink : class
+ => Library.UnregisterLinkedSyncObject(syncObject);
+
+ ///
+ /// Removes the registered MonkeySync
+ /// object that was linked to the given link object,
+ /// so that it can't be accessed through a reference to it anymore.
+ /// Optionally disposes of the sync object if necessary.
+ ///
+ /// The type of the link object used by the sync object.
+ /// The link object used by the sync object.
+ /// Whether to dispose the dispose of the sync object if necessary.
+ /// true if a sync object was registered for the given link object; otherwise, false.
+ public static bool UnregisterLinkedSyncObject(TLink linkObject, bool dispose = false)
+ where TLink : class
+ => Library.UnregisterLinkedSyncObject(linkObject, dispose);
+
+ ///
+ /// Removes the sync object type with the given .
+ ///
+ /// The type of the link object used by the sync object.
+ /// The name of the registered sync object.
+ ///
+ /// true if the registered sync object type with the given was removed; otherwise, false.
+ ///
+ ///
+ /// When there's a registered sync object type with the given ,
+ /// but none with the associated - or there's a Name mismatch.
+ ///
+ public static bool UnregisterSyncObject(string name)
+ where TLink : class
+ => Library.UnregisterSyncObject(name);
+
+ ///
+ /// Removes the sync object type for the given .
+ ///
+ /// The type of the link object used by the sync object.
+ /// The type of the registered sync object.
+ ///
+ /// true if the registered sync object type for the given was removed; otherwise, false.
+ ///
+ ///
+ /// When there's a registered sync object type for the given ,
+ /// but none with the associated Name - or there's a Type mismatch.
+ ///
+ public static bool UnregisterSyncObject(Type syncObjectType)
+ where TLink : class
+ => Library.UnregisterSyncObject(syncObjectType);
+
+ private static class Library
+ where TLink : class
+ {
+ private static readonly Dictionary> _registeredObjectByType = [];
+ private static readonly Dictionary> _registeredObjectsByName = new(StringComparer.Ordinal);
+ private static readonly ConditionalWeakTable> _syncObjectsByLinkObject = new();
+
+ public static MonkeySyncObjectRegistration GetSyncObjectRegistration(Type type)
+ => _registeredObjectByType[type];
+
+ public static MonkeySyncObjectRegistration GetSyncObjectRegistration(string name)
+ => _registeredObjectsByName[name];
+
+ public static void RegisterLinkedSyncObject(ILinkedMonkeySyncObject syncObject)
+ => _syncObjectsByLinkObject.Add(syncObject.LinkObject, syncObject);
+
+ public static MonkeySyncObjectRegistration RegisterSyncObject(string name, Type syncObjectType, SyncObjectFactory createSyncObject)
+ {
+ if (_registeredObjectsByName.ContainsKey(name) || _registeredObjectByType.ContainsKey(syncObjectType))
+ throw new ArgumentException($"Sync Object type [{syncObjectType.CompactDescription()}] with name [{name}] has already been registered!");
+
+ var registeredObject = new MonkeySyncObjectRegistration(name, syncObjectType, createSyncObject);
+
+ _registeredObjectsByName.Add(name, registeredObject);
+ _registeredObjectByType.Add(syncObjectType, registeredObject);
+
+ return registeredObject;
+ }
+
+ public static bool TryGetLinkedSyncObject(TLink linkObject, [NotNullWhen(true)] out ILinkedMonkeySyncObject? syncObject)
+ => _syncObjectsByLinkObject.TryGetValue(linkObject, out syncObject);
+
+ public static bool TryGetSyncObjectRegistration(string name, [NotNullWhen(true)] out MonkeySyncObjectRegistration? registeredSyncObject)
+ => _registeredObjectsByName.TryGetValue(name, out registeredSyncObject);
+
+ public static bool TryGetSyncObjectRegistration(Type type, [NotNullWhen(true)] out MonkeySyncObjectRegistration? registeredSyncObject)
+ => _registeredObjectByType.TryGetValue(type, out registeredSyncObject);
+
+ public static bool UnregisterLinkedSyncObject(TLink linkObject, bool dispose)
+ {
+ if (!dispose)
+ return _syncObjectsByLinkObject.Remove(linkObject);
+
+ if (!_syncObjectsByLinkObject.TryGetValue(linkObject, out var syncObject))
+ return false;
+
+ (syncObject as IDisposable)?.Dispose();
+
+ return true;
+ }
+
+ public static bool UnregisterLinkedSyncObject(ILinkedMonkeySyncObject syncObject)
+ => _syncObjectsByLinkObject.Remove(syncObject.LinkObject);
+
+ public static bool UnregisterSyncObject(string name)
+ {
+ if (!TryGetSyncObjectRegistration(name, out var registeredObject))
+ return false;
+
+ if (!TryGetSyncObjectRegistration(registeredObject.SyncObjectType, out var registeredObject2) || registeredObject.Name != registeredObject2.Name)
+ throw new InvalidOperationException($"No Sync Object type found using type [{registeredObject.SyncObjectType.CompactDescription()} based on the name [{name}], or name [{registeredObject2?.Name ?? "N/A"}] doesn't match!");
+
+ _registeredObjectsByName.Remove(name);
+ _registeredObjectByType.Remove(registeredObject.SyncObjectType);
+
+ return true;
+ }
+
+ public static bool UnregisterSyncObject(Type syncObjectType)
+ {
+ if (!TryGetSyncObjectRegistration(syncObjectType, out var registeredObject))
+ return false;
+
+ if (!TryGetSyncObjectRegistration(registeredObject.Name, out var registeredObject2) || registeredObject.SyncObjectType != registeredObject2.SyncObjectType)
+ throw new InvalidOperationException($"No Sync Object type found using name [{registeredObject.Name} based on the type [{syncObjectType.CompactDescription()}], or type [{registeredObject2?.SyncObjectType.CompactDescription() ?? "N/A"}] doesn't match!");
+
+ _registeredObjectByType.Remove(syncObjectType);
+ _registeredObjectsByName.Remove(registeredObject.Name);
+
+ return true;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/MonkeyLoader/Sync/MonkeySyncValue.cs b/MonkeyLoader/Sync/MonkeySyncValue.cs
new file mode 100644
index 0000000..e1d0b91
--- /dev/null
+++ b/MonkeyLoader/Sync/MonkeySyncValue.cs
@@ -0,0 +1,338 @@
+using EnumerableToolkit;
+using MonkeyLoader.Meta;
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
+
+namespace MonkeyLoader.Sync
+{
+ ///
+ /// Defines the non-generic interface for s.
+ ///
+ ///
+ public interface ILinkedMonkeySyncValue : INotifyValueChanged, IDisposable
+ where TSyncObject : ILinkedMonkeySyncObject
+ {
+ ///
+ /// Gets the LinkObject
+ /// of the SyncObject that this value belongs to.
+ ///
+ public TLink LinkObject { get; }
+
+ ///
+ /// Gets the property name of this sync value.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Gets the sync object that this value belongs to.
+ ///
+ public TSyncObject SyncObject { get; }
+
+ ///
+ /// Gets or sets the internal value of this sync value.
+ ///
+ public object? Value { get; set; }
+
+ ///
+ /// Gets the concrete of
+ /// the wrapped Value.
+ ///
+ public Type ValueType { get; }
+
+ ///
+ /// Tries to restore the link of this sync value.
+ ///
+ /// true if the link was successfully restored; otherwise, false.
+ public bool TryRestoreLink();
+ }
+
+ ///
+ /// Defines the generic interface for linked s.
+ ///
+ /// The type of the link object used by the sync object that this sync value links to.
+ /// The type of the sync object that may contain this sync value.
+ /// The type of the Value.
+ public interface ILinkedMonkeySyncValue : INotifyValueChanged,
+ IReadOnlyMonkeySyncValue, IWriteOnlyMonkeySyncValue
+ where TSyncObject : ILinkedMonkeySyncObject
+ {
+ ///
+ public new T Value { get; set; }
+ }
+
+ ///
+ /// Defines the interface for readonly s.
+ ///
+ ///
+ /// This interface exists purely to facilitate keeping a covariant list of sync values.
+ ///
+ ///
+ public interface IReadOnlyMonkeySyncValue : ILinkedMonkeySyncValue
+ where TSyncObject : ILinkedMonkeySyncObject
+ {
+ ///
+ /// Gets the internal value of this sync value.
+ ///
+ public new T Value { get; }
+ }
+
+ ///
+ /// Defines the interface for not yet linked s.
+ ///
+ ///
+ public interface IUnlinkedMonkeySyncValue
+ where TSyncObject : ILinkedMonkeySyncObject
+ where TLinkedSyncValue : ILinkedMonkeySyncValue
+ {
+ ///
+ /// Establishes this sync value's association and link through the given sync object.
+ ///
+ /// The sync object that this value belongs to.
+ /// The name of this sync value.
+ /// Whether the link is being established from the remote side.
+ /// The linked sync value if the established link is valid; otherwise, null.
+ public TLinkedSyncValue? EstablishLinkFor(TSyncObject syncObject, string name, bool fromRemote);
+ }
+
+ ///
+ /// Defines the interface for writeonly s.
+ ///
+ ///
+ /// This interface exists purely to facilitate keeping a contravariant list of sync values.
+ ///
+ ///
+ public interface IWriteOnlyMonkeySyncValue : ILinkedMonkeySyncValue
+ where TSyncObject : ILinkedMonkeySyncObject
+ {
+ ///
+ /// Sets the internal value of this sync value.
+ ///
+ public new T Value { set; }
+ }
+
+ ///
+ /// Implements an abstract base for s.
+ ///
+ /// The type of the link object used by the sync object that this sync value links to.
+ /// The type of the sync object that may contain this sync value.
+ /// The concrete type of the sync value.
+ /// The type of the Value.
+ public abstract class MonkeySyncValue
+ : IUnlinkedMonkeySyncValue,
+ ILinkedMonkeySyncValue
+ where TSyncObject : class, ILinkedMonkeySyncObject
+ where TSyncValue : MonkeySyncValue
+ {
+ private static readonly Type _valueType = typeof(T);
+
+ private bool _disposedValue;
+ private ValueChangedEventHandler? _untypedChanged;
+ private T _value;
+
+ ///
+ public TLink LinkObject => SyncObject.LinkObject;
+
+ ///
+ public string Name { get; private set; } = null!;
+
+ ///
+ public TSyncObject SyncObject { get; private set; } = null!;
+
+ ///
+ public virtual T Value
+ {
+ get => _value;
+
+ [MemberNotNull(nameof(_value))]
+ set
+ {
+ var oldValue = _value;
+ _value = value!;
+
+ OnChanged(oldValue);
+ }
+ }
+
+ object? ILinkedMonkeySyncValue.Value
+ {
+ get => Value;
+ set => Value = (T)value!;
+ }
+
+ ///
+ public Type ValueType => _valueType;
+
+ ///
+ /// Creates a new sync value instance that wraps the given .
+ ///
+ /// The value to wrap.
+ public MonkeySyncValue(T value)
+ {
+ Value = value;
+ }
+
+ ///
+ /// Ensures any unmanaged resources are disposed.
+ ///
+ ~MonkeySyncValue()
+ {
+ Dispose(false);
+ }
+
+ ///
+ /// Unwraps the Value from the given sync object.
+ ///
+ /// The sync object to unwrap.
+ public static implicit operator T(MonkeySyncValue syncValue) => syncValue.Value;
+
+ ///
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in the 'OnDisposing()' or 'OnFinalizing()' methods
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Sets this sync value's SyncObject
+ /// and Name to the ones provided.
+ /// Then calls the internal link method.
+ ///
+ ///
+ public TSyncValue? EstablishLinkFor(TSyncObject syncObject, string propertyName, bool fromRemote)
+ {
+ SyncObject = syncObject;
+ Name = propertyName;
+
+ return (TSyncValue?)(EstablishLinkInternal(fromRemote) ? this : null);
+ }
+
+ ///
+ public override string ToString() => Value?.ToString() ?? "";
+
+ ///
+ public abstract bool TryRestoreLink();
+
+ ///
+ /// Handles the aspects of establishing a link that are
+ /// particular to s as a link object.
+ /// The SyncObject and Name
+ /// have already been assigned by the EstablishLinkFor
+ /// method that calls this.
+ ///
+ ///
+ protected abstract bool EstablishLinkInternal(bool fromRemote);
+
+ ///
+ /// Cleans up any managed resources as part of disposing.
+ ///
+ ///
+ /// By default: Sets the SyncObject to null
+ /// and the internal value to its default.
+ ///
+ protected virtual void OnDisposing()
+ {
+ SyncObject = null!;
+ _value = default!;
+ }
+
+ ///
+ /// Cleans up any unmanaged resources as part of
+ /// disposing or finalization.
+ ///
+ protected virtual void OnFinalizing()
+ { }
+
+ private void Dispose(bool disposing)
+ {
+ if (_disposedValue)
+ return;
+
+ if (disposing)
+ OnDisposing();
+
+ OnFinalizing();
+
+ _disposedValue = true;
+ }
+
+ ///
+ /// Handles the value of this config item potentially having changed.
+ /// If the and new value are different:
+ /// Triggers this sync object's typed and untyped Changed events.
+ ///
+ /// The old value.
+ /// The name of the changed property on the value.
+ /// The collection change arguments for the value.
+ private void OnChanged(T? oldValue, string? changedProperty = null, NotifyCollectionChangedEventArgs? changedCollection = null)
+ {
+ var sameReferences = ReferenceEquals(oldValue, _value);
+
+ if (!sameReferences)
+ {
+ // Remove NotifyChanged integration from old value
+ if (oldValue is INotifyPropertyChanged oldPropertyChanged)
+ oldPropertyChanged.PropertyChanged -= ValuePropertyChanged;
+
+ if (oldValue is INotifyCollectionChanged oldCollectionChanged)
+ oldCollectionChanged.CollectionChanged -= ValueCollectionChanged;
+
+ // Add NotifyChanged integration to new value
+ if (_value is INotifyPropertyChanged newPropertyChanged)
+ newPropertyChanged.PropertyChanged += ValuePropertyChanged;
+
+ if (_value is INotifyCollectionChanged newCollectionChanged)
+ newCollectionChanged.CollectionChanged += ValueCollectionChanged;
+ }
+
+ // Don't fire event if it wasn't triggered by event and the value didn't change
+ if ((sameReferences && changedProperty is null && changedCollection is null)
+ || (oldValue is not null && _value is not null && _value.Equals(oldValue)))
+ return;
+
+ var eventArgs = new ValueChangedEventArgs(oldValue, _value, changedProperty, changedCollection);
+
+ var exceptions = new List();
+
+ try
+ {
+ Changed?.TryInvokeAll(this, eventArgs);
+ }
+ catch (AggregateException ex)
+ {
+ exceptions.Add(ex);
+ }
+
+ try
+ {
+ _untypedChanged?.TryInvokeAll(this, eventArgs);
+ }
+ catch (AggregateException ex)
+ {
+ exceptions.Add(ex);
+ }
+
+ if (exceptions.Count > 0)
+ throw new AggregateException($"Some {nameof(Changed)} event subscriber(s) of this MonkeySyncValue threw an exception.", exceptions);
+ }
+
+ private void ValueCollectionChanged(object sender, NotifyCollectionChangedEventArgs eventArgs)
+ => OnChanged(_value, null, eventArgs);
+
+ private void ValuePropertyChanged(object sender, PropertyChangedEventArgs eventArgs)
+ => OnChanged(_value, eventArgs.PropertyName);
+
+ ///
+ public event ValueChangedEventHandler? Changed;
+
+ event ValueChangedEventHandler? INotifyValueChanged.Changed
+ {
+ add => _untypedChanged += value;
+ remove => _untypedChanged -= value;
+ }
+ }
+}
\ No newline at end of file