Skip to content

Instantly share code, notes, and snippets.

@luttje
Last active May 5, 2025 17:08
Show Gist options
  • Save luttje/f54109d9753293cd1b059c603fada5df to your computer and use it in GitHub Desktop.
Save luttje/f54109d9753293cd1b059c603fada5df to your computer and use it in GitHub Desktop.
A copy of the SyncDictionary in Mirror Networking (Unity), except this will update a synced instance when an INotifyPropertyChanged event is raised
using Mirror;
using System;
using System.ComponentModel;
using UnityEngine;
public class CastableSkill : INotifyPropertyChanged
{
public SkillData skillData;
public double nextCastTime;
public uint currentPoints;
public byte slotId;
public event PropertyChangedEventHandler PropertyChanged;
internal bool IsOnCooldown()
{
return NetworkTime.time < nextCastTime;
}
internal float GetRemainingCooldown()
{
var remainingCooldown = nextCastTime - NetworkTime.time;
return (float)(remainingCooldown > 0 ? remainingCooldown : 0f);
}
internal void StartCooldown()
{
nextCastTime = NetworkTime.time + skillData.cooldownTime;
OnPropertyChanged(nameof(nextCastTime));
}
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
using Mirror;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
/// <summary>
/// This is a copy and modification of SyncIDictionary in Mirror. It adds updating instances of items.
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TValue"></typeparam>
public class ExtendedSyncIDictionary<TKey, TValue> : SyncObject, IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue> where TValue : INotifyPropertyChanged
{
/// <summary>This is called after the item is added with TKey</summary>
public Action<TKey> OnAdd;
/// <summary>This is called after the item is changed with TKey.</summary>
public Action<TKey, TValue, TValue> OnSet;
/// <summary>This is called after the item's properties are changed with TKey.</summary>
public Action<TKey, TValue> OnUpdate;
/// <summary>This is called after the item is removed with TKey.</summary>
public Action<TKey, TValue, TValue> OnRemove;
/// <summary>This is called before the data is cleared</summary>
public Action OnClear;
public enum Operation : byte
{
OP_ADD,
OP_SET,
OP_UPDATE,
OP_REMOVE,
OP_CLEAR
}
/// <summary>
/// This is called for all changes to the Dictionary.
/// <para>For OP_ADD, TValue is the NEW value of the entry.</para>
/// <para>For OP_SET and OP_REMOVE, TValue is the OLD value of the entry.</para>
/// <para>For OP_CLEAR, both TKey and TValue are default.</para>
/// </summary>
public Action<Operation, TKey, TValue> OnChange;
protected readonly IDictionary<TKey, TValue> objects;
public ExtendedSyncIDictionary(IDictionary<TKey, TValue> objects)
{
this.objects = objects;
AttachListeners(this.objects);
}
public int Count => objects.Count;
public bool IsReadOnly => !IsWritable();
struct Change
{
internal Operation operation;
internal TKey key;
internal TValue item;
}
// list of changes.
// -> insert/delete/clear is only ONE change
// -> changing the same slot 10x causes 10 changes.
// -> note that this grows until next sync(!)
// TODO Dictionary<key, change> to avoid ever growing changes / redundant changes!
readonly List<Change> changes = new();
// how many changes we need to ignore
// this is needed because when we initialize the list,
// we might later receive changes that have already been applied
// so we need to skip them
int changesAhead;
public ICollection<TKey> Keys => objects.Keys;
public ICollection<TValue> Values => objects.Values;
IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys => objects.Keys;
IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values => objects.Values;
public override void OnSerializeAll(NetworkWriter writer)
{
// if init, write the full list content
writer.WriteUInt((uint)objects.Count);
foreach (KeyValuePair<TKey, TValue> syncItem in objects)
{
writer.Write(syncItem.Key);
writer.Write(syncItem.Value);
}
// all changes have been applied already
// thus the client will need to skip all the pending changes
// or they would be applied again.
// So we write how many changes are pending
writer.WriteUInt((uint)changes.Count);
}
public override void OnSerializeDelta(NetworkWriter writer)
{
// write all the queued up changes
writer.WriteUInt((uint)changes.Count);
for (int i = 0; i < changes.Count; i++)
{
Change change = changes[i];
writer.WriteByte((byte)change.operation);
switch (change.operation)
{
case Operation.OP_ADD:
case Operation.OP_SET:
case Operation.OP_UPDATE:
writer.Write(change.key);
writer.Write(change.item);
break;
case Operation.OP_REMOVE:
writer.Write(change.key);
break;
case Operation.OP_CLEAR:
break;
}
}
}
public override void OnDeserializeAll(NetworkReader reader)
{
// if init, write the full list content
int count = (int)reader.ReadUInt();
objects.Clear();
changes.Clear();
for (int i = 0; i < count; i++)
{
TKey key = reader.Read<TKey>();
TValue obj = reader.Read<TValue>();
objects.Add(key, obj);
}
// We will need to skip all these changes
// the next time the list is synchronized
// because they have already been applied
changesAhead = (int)reader.ReadUInt();
}
public override void OnDeserializeDelta(NetworkReader reader)
{
int changesCount = (int)reader.ReadUInt();
for (int i = 0; i < changesCount; i++)
{
Operation operation = (Operation)reader.ReadByte();
// apply the operation only if it is a new change
// that we have not applied yet
bool apply = changesAhead == 0;
TKey key = default;
TValue item = default;
switch (operation)
{
case Operation.OP_ADD:
case Operation.OP_SET:
case Operation.OP_UPDATE:
key = reader.Read<TKey>();
item = reader.Read<TValue>();
if (apply)
{
// add dirty + changes.
// ClientToServer needs to set dirty in server OnDeserialize.
// no access check: server OnDeserialize can always
// write, even for ClientToServer (for broadcasting).
if (objects.TryGetValue(key, out TValue oldItem))
{
if (operation == Operation.OP_SET)
{
objects[key] = item; // assign after TryGetValue
AddOperation(Operation.OP_SET, key, item, oldItem, false);
}
else if (operation == Operation.OP_UPDATE)
{
// Change the properties and fields of the item so the reference
// stays the same.
var itemType = typeof(TValue);
var properties = itemType.GetProperties();
foreach (var property in properties)
{
if (property.CanWrite)
{
var newValue = property.GetValue(item);
property.SetValue(oldItem, newValue);
}
}
var fields = itemType.GetFields();
foreach (var field in fields)
{
var newValue = field.GetValue(item);
field.SetValue(oldItem, newValue);
}
AddOperation(Operation.OP_UPDATE, key, oldItem, oldItem, false);
}
}
else
{
objects[key] = item; // assign after TryGetValue
AddOperation(Operation.OP_ADD, key, item, default, false);
}
}
break;
case Operation.OP_CLEAR:
if (apply)
{
// add dirty + changes.
// ClientToServer needs to set dirty in server OnDeserialize.
// no access check: server OnDeserialize can always
// write, even for ClientToServer (for broadcasting).
AddOperation(Operation.OP_CLEAR, default, default, default, false);
// clear after invoking the callback so users can iterate the dictionary
// and take appropriate action on the items before they are wiped.
objects.Clear();
}
break;
case Operation.OP_REMOVE:
key = reader.Read<TKey>();
if (apply)
{
if (objects.TryGetValue(key, out TValue oldItem))
{
// add dirty + changes.
// ClientToServer needs to set dirty in server OnDeserialize.
// no access check: server OnDeserialize can always
// write, even for ClientToServer (for broadcasting).
objects.Remove(key);
AddOperation(Operation.OP_REMOVE, key, oldItem, oldItem, false);
}
}
break;
}
if (!apply)
{
// we just skipped this change
changesAhead--;
}
}
}
// throw away all the changes
// this should be called after a successful sync
public override void ClearChanges() => changes.Clear();
public override void Reset()
{
changes.Clear();
changesAhead = 0;
objects.Clear();
}
public TValue this[TKey i]
{
get => objects[i];
set
{
if (ContainsKey(i))
{
TValue oldItem = objects[i];
if (oldItem != null)
DetachListener(oldItem);
objects[i] = value;
AddOperation(Operation.OP_SET, i, value, oldItem, true);
}
else
{
objects[i] = value;
AddOperation(Operation.OP_ADD, i, value, default, true);
}
AttachListener(value);
}
}
protected void AttachListener(TValue item)
{
item.PropertyChanged += OnItemPropertyChanged;
}
protected void AttachListeners(IDictionary<TKey, TValue> items)
{
foreach (var item in items.Values)
{
AttachListener(item);
}
}
protected void DetachListener(TValue item)
{
item.PropertyChanged -= OnItemPropertyChanged;
}
protected void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (sender is not TValue item)
return;
foreach (var kvp in objects)
{
if (kvp.Value.Equals(item))
{
// TODO: Only update the property that changed?
AddOperation(Operation.OP_UPDATE, kvp.Key, item, item, true);
break;
}
}
}
public bool TryGetValue(TKey key, out TValue value) => objects.TryGetValue(key, out value);
public bool ContainsKey(TKey key) => objects.ContainsKey(key);
public bool Contains(KeyValuePair<TKey, TValue> item) => TryGetValue(item.Key, out TValue val) && EqualityComparer<TValue>.Default.Equals(val, item.Value);
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
{
if (arrayIndex < 0 || arrayIndex > array.Length)
throw new ArgumentOutOfRangeException(nameof(arrayIndex), "Array Index Out of Range");
if (array.Length - arrayIndex < Count)
throw new ArgumentException("The number of items in the SyncDictionary is greater than the available space from arrayIndex to the end of the destination array");
int i = arrayIndex;
foreach (KeyValuePair<TKey, TValue> item in objects)
{
array[i] = item;
i++;
}
}
public void Add(KeyValuePair<TKey, TValue> item) => Add(item.Key, item.Value);
public void Add(TKey key, TValue value)
{
objects.Add(key, value);
AddOperation(Operation.OP_ADD, key, value, default, true);
AttachListener(value);
}
public bool Remove(TKey key)
{
if (objects.TryGetValue(key, out TValue oldItem) && objects.Remove(key))
{
AddOperation(Operation.OP_REMOVE, key, oldItem, oldItem, true);
DetachListener(oldItem);
return true;
}
return false;
}
public bool Remove(KeyValuePair<TKey, TValue> item)
{
bool result = objects.Remove(item.Key);
if (result)
{
AddOperation(Operation.OP_REMOVE, item.Key, item.Value, item.Value, true);
DetachListener(item.Value);
}
return result;
}
public void Clear()
{
AddOperation(Operation.OP_CLEAR, default, default, default, true);
// clear after invoking the callback so users can iterate the dictionary
// and take appropriate action on the items before they are wiped.
foreach (var item in objects.Values)
{
DetachListener(item);
}
objects.Clear();
}
void AddOperation(Operation op, TKey key, TValue item, TValue oldItem, bool checkAccess)
{
if (checkAccess && IsReadOnly)
throw new InvalidOperationException("SyncDictionaries can only be modified by the owner.");
Change change = new()
{
operation = op,
key = key,
item = item
};
if (IsRecording())
{
changes.Add(change);
OnDirty?.Invoke();
}
switch (op)
{
case Operation.OP_ADD:
OnAdd?.Invoke(key);
OnChange?.Invoke(op, key, item);
break;
case Operation.OP_SET:
OnSet?.Invoke(key, oldItem, item);
OnChange?.Invoke(op, key, oldItem);
break;
case Operation.OP_UPDATE:
OnUpdate?.Invoke(key, oldItem);
OnChange?.Invoke(op, key, oldItem);
break;
case Operation.OP_REMOVE:
OnRemove?.Invoke(key, oldItem, item);
OnChange?.Invoke(op, key, oldItem);
break;
case Operation.OP_CLEAR:
OnClear?.Invoke();
OnChange?.Invoke(op, default, default);
break;
}
}
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => objects.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => objects.GetEnumerator();
}
public class ExtendedSyncDictionary<TKey, TValue> : ExtendedSyncIDictionary<TKey, TValue> where TValue : INotifyPropertyChanged
{
public ExtendedSyncDictionary() : base(new Dictionary<TKey, TValue>()) { }
public ExtendedSyncDictionary(IEqualityComparer<TKey> eq) : base(new Dictionary<TKey, TValue>(eq)) { }
public ExtendedSyncDictionary(IDictionary<TKey, TValue> d) : base(new Dictionary<TKey, TValue>(d)) { }
public new Dictionary<TKey, TValue>.ValueCollection Values => ((Dictionary<TKey, TValue>)objects).Values;
public new Dictionary<TKey, TValue>.KeyCollection Keys => ((Dictionary<TKey, TValue>)objects).Keys;
public new Dictionary<TKey, TValue>.Enumerator GetEnumerator() => ((Dictionary<TKey, TValue>)objects).GetEnumerator();
}
///
/// This example shows a variant of the ExtendedSyncDictionary that doesn't use
/// reflection and gives you more control over how you apply the updates to your
/// object.
///
// CastableSkill.cs
using Mirror;
using System;
using UnityEngine;
public class CastableSkill : ISyncObjectUpdates<CastableSkill>
{
public SkillData skillData;
public double nextCastTime;
public uint currentPoints;
public byte slotId;
public event SyncObjectChangedEventHandler SyncObjectChanged;
internal bool IsOnCooldown()
{
return NetworkTime.time < nextCastTime;
}
internal float GetRemainingCooldown()
{
var remainingCooldown = nextCastTime - NetworkTime.time;
return (float)(remainingCooldown > 0 ? remainingCooldown : 0f);
}
internal void StartCooldown()
{
nextCastTime = NetworkTime.time + skillData.cooldownTime;
OnSyncObjectChanged();
}
private void OnSyncObjectChanged()
{
SyncObjectChanged?.Invoke(this, new SyncObjectChangedEventArgs());
}
public void ApplyUpdate(CastableSkill newValuesToAssume)
{
skillData = newValuesToAssume.skillData;
nextCastTime = newValuesToAssume.nextCastTime;
currentPoints = newValuesToAssume.currentPoints;
slotId = newValuesToAssume.slotId;
}
}
// ExtendedSyncDictionary.cs
using Mirror;
using System;
using System.Collections;
using System.Collections.Generic;
public delegate void SyncObjectChangedEventHandler(object sender, SyncObjectChangedEventArgs e);
public class SyncObjectChangedEventArgs : EventArgs
{
public SyncObjectChangedEventArgs() { }
}
/// <summary>
/// Apply this interface to any object you want to sync with ExtendedSyncDictionary.
/// </summary>
/// <typeparam name="TValue"></typeparam>
public interface ISyncObjectUpdates<TValue>
{
/// <summary>
/// Call this when the object changes.
/// </summary>
event SyncObjectChangedEventHandler SyncObjectChanged;
/// <summary>
/// This is called when a new update is received. Copy the values from
/// the new object to 'this' instance.
/// </summary>
/// <param name="newValuesToAssume"></param>
void ApplyUpdate(TValue newValuesToAssume);
}
/// <summary>
/// This is a copy and modification of SyncIDictionary in Mirror. It adds updating instances of items.
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TValue"></typeparam>
public class ExtendedSyncIDictionary<TKey, TValue> : SyncObject, IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue> where TValue : ISyncObjectUpdates<TValue>
{
/// <summary>This is called after the item is added with TKey</summary>
public Action<TKey> OnAdd;
/// <summary>This is called after the item is changed with TKey.</summary>
public Action<TKey, TValue, TValue> OnSet;
/// <summary>This is called after the item's properties are changed with TKey.</summary>
public Action<TKey, TValue> OnUpdate;
/// <summary>This is called after the item is removed with TKey.</summary>
public Action<TKey, TValue, TValue> OnRemove;
/// <summary>This is called before the data is cleared</summary>
public Action OnClear;
public enum Operation : byte
{
OP_ADD,
OP_SET,
OP_UPDATE,
OP_REMOVE,
OP_CLEAR
}
/// <summary>
/// This is called for all changes to the Dictionary.
/// <para>For OP_ADD, TValue is the NEW value of the entry.</para>
/// <para>For OP_SET and OP_REMOVE, TValue is the OLD value of the entry.</para>
/// <para>For OP_CLEAR, both TKey and TValue are default.</para>
/// </summary>
public Action<Operation, TKey, TValue> OnChange;
protected readonly IDictionary<TKey, TValue> objects;
public ExtendedSyncIDictionary(IDictionary<TKey, TValue> objects)
{
this.objects = objects;
AttachListeners(this.objects);
}
public int Count => objects.Count;
public bool IsReadOnly => !IsWritable();
struct Change
{
internal Operation operation;
internal TKey key;
internal TValue item;
}
// list of changes.
// -> insert/delete/clear is only ONE change
// -> changing the same slot 10x causes 10 changes.
// -> note that this grows until next sync(!)
// TODO Dictionary<key, change> to avoid ever growing changes / redundant changes!
readonly List<Change> changes = new();
// how many changes we need to ignore
// this is needed because when we initialize the list,
// we might later receive changes that have already been applied
// so we need to skip them
int changesAhead;
public ICollection<TKey> Keys => objects.Keys;
public ICollection<TValue> Values => objects.Values;
IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys => objects.Keys;
IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values => objects.Values;
public override void OnSerializeAll(NetworkWriter writer)
{
// if init, write the full list content
writer.WriteUInt((uint)objects.Count);
foreach (KeyValuePair<TKey, TValue> syncItem in objects)
{
writer.Write(syncItem.Key);
writer.Write(syncItem.Value);
}
// all changes have been applied already
// thus the client will need to skip all the pending changes
// or they would be applied again.
// So we write how many changes are pending
writer.WriteUInt((uint)changes.Count);
}
public override void OnSerializeDelta(NetworkWriter writer)
{
// write all the queued up changes
writer.WriteUInt((uint)changes.Count);
for (int i = 0; i < changes.Count; i++)
{
Change change = changes[i];
writer.WriteByte((byte)change.operation);
switch (change.operation)
{
case Operation.OP_ADD:
case Operation.OP_SET:
case Operation.OP_UPDATE:
writer.Write(change.key);
writer.Write(change.item);
break;
case Operation.OP_REMOVE:
writer.Write(change.key);
break;
case Operation.OP_CLEAR:
break;
}
}
}
public override void OnDeserializeAll(NetworkReader reader)
{
// if init, write the full list content
int count = (int)reader.ReadUInt();
objects.Clear();
changes.Clear();
for (int i = 0; i < count; i++)
{
TKey key = reader.Read<TKey>();
TValue obj = reader.Read<TValue>();
objects.Add(key, obj);
}
// We will need to skip all these changes
// the next time the list is synchronized
// because they have already been applied
changesAhead = (int)reader.ReadUInt();
}
public override void OnDeserializeDelta(NetworkReader reader)
{
int changesCount = (int)reader.ReadUInt();
for (int i = 0; i < changesCount; i++)
{
Operation operation = (Operation)reader.ReadByte();
// apply the operation only if it is a new change
// that we have not applied yet
bool apply = changesAhead == 0;
TKey key = default;
TValue item = default;
switch (operation)
{
case Operation.OP_ADD:
case Operation.OP_SET:
case Operation.OP_UPDATE:
key = reader.Read<TKey>();
item = reader.Read<TValue>();
if (apply)
{
// add dirty + changes.
// ClientToServer needs to set dirty in server OnDeserialize.
// no access check: server OnDeserialize can always
// write, even for ClientToServer (for broadcasting).
if (objects.TryGetValue(key, out TValue oldItem))
{
if (operation == Operation.OP_SET)
{
objects[key] = item; // assign after TryGetValue
AddOperation(Operation.OP_SET, key, item, oldItem, false);
}
else if (operation == Operation.OP_UPDATE)
{
// Change the properties and fields of the item so the reference
// stays the same.
oldItem.ApplyUpdate(item);
AddOperation(Operation.OP_UPDATE, key, oldItem, oldItem, false);
}
}
else
{
objects[key] = item; // assign after TryGetValue
AddOperation(Operation.OP_ADD, key, item, default, false);
}
}
break;
case Operation.OP_CLEAR:
if (apply)
{
// add dirty + changes.
// ClientToServer needs to set dirty in server OnDeserialize.
// no access check: server OnDeserialize can always
// write, even for ClientToServer (for broadcasting).
AddOperation(Operation.OP_CLEAR, default, default, default, false);
// clear after invoking the callback so users can iterate the dictionary
// and take appropriate action on the items before they are wiped.
objects.Clear();
}
break;
case Operation.OP_REMOVE:
key = reader.Read<TKey>();
if (apply)
{
if (objects.TryGetValue(key, out TValue oldItem))
{
// add dirty + changes.
// ClientToServer needs to set dirty in server OnDeserialize.
// no access check: server OnDeserialize can always
// write, even for ClientToServer (for broadcasting).
objects.Remove(key);
AddOperation(Operation.OP_REMOVE, key, oldItem, oldItem, false);
}
}
break;
}
if (!apply)
{
// we just skipped this change
changesAhead--;
}
}
}
// throw away all the changes
// this should be called after a successful sync
public override void ClearChanges() => changes.Clear();
public override void Reset()
{
changes.Clear();
changesAhead = 0;
objects.Clear();
}
public TValue this[TKey i]
{
get => objects[i];
set
{
if (ContainsKey(i))
{
TValue oldItem = objects[i];
if (oldItem != null)
DetachListener(oldItem);
objects[i] = value;
AddOperation(Operation.OP_SET, i, value, oldItem, true);
}
else
{
objects[i] = value;
AddOperation(Operation.OP_ADD, i, value, default, true);
}
AttachListener(value);
}
}
protected void AttachListener(TValue item)
{
item.SyncObjectChanged += OnItemChanged;
}
protected void AttachListeners(IDictionary<TKey, TValue> items)
{
foreach (var item in items.Values)
{
AttachListener(item);
}
}
protected void DetachListener(TValue item)
{
item.SyncObjectChanged -= OnItemChanged;
}
protected void OnItemChanged(object sender, SyncObjectChangedEventArgs e)
{
if (sender is not TValue item)
return;
foreach (var kvp in objects)
{
if (kvp.Value.Equals(item))
{
AddOperation(Operation.OP_UPDATE, kvp.Key, item, item, true);
break;
}
}
}
public bool TryGetValue(TKey key, out TValue value) => objects.TryGetValue(key, out value);
public bool ContainsKey(TKey key) => objects.ContainsKey(key);
public bool Contains(KeyValuePair<TKey, TValue> item) => TryGetValue(item.Key, out TValue val) && EqualityComparer<TValue>.Default.Equals(val, item.Value);
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
{
if (arrayIndex < 0 || arrayIndex > array.Length)
throw new ArgumentOutOfRangeException(nameof(arrayIndex), "Array Index Out of Range");
if (array.Length - arrayIndex < Count)
throw new ArgumentException("The number of items in the SyncDictionary is greater than the available space from arrayIndex to the end of the destination array");
int i = arrayIndex;
foreach (KeyValuePair<TKey, TValue> item in objects)
{
array[i] = item;
i++;
}
}
public void Add(KeyValuePair<TKey, TValue> item) => Add(item.Key, item.Value);
public void Add(TKey key, TValue value)
{
objects.Add(key, value);
AddOperation(Operation.OP_ADD, key, value, default, true);
AttachListener(value);
}
public bool Remove(TKey key)
{
if (objects.TryGetValue(key, out TValue oldItem) && objects.Remove(key))
{
AddOperation(Operation.OP_REMOVE, key, oldItem, oldItem, true);
DetachListener(oldItem);
return true;
}
return false;
}
public bool Remove(KeyValuePair<TKey, TValue> item)
{
bool result = objects.Remove(item.Key);
if (result)
{
AddOperation(Operation.OP_REMOVE, item.Key, item.Value, item.Value, true);
DetachListener(item.Value);
}
return result;
}
public void Clear()
{
AddOperation(Operation.OP_CLEAR, default, default, default, true);
// clear after invoking the callback so users can iterate the dictionary
// and take appropriate action on the items before they are wiped.
foreach (var item in objects.Values)
{
DetachListener(item);
}
objects.Clear();
}
void AddOperation(Operation op, TKey key, TValue item, TValue oldItem, bool checkAccess)
{
if (checkAccess && IsReadOnly)
throw new InvalidOperationException("SyncDictionaries can only be modified by the owner.");
Change change = new()
{
operation = op,
key = key,
item = item
};
if (IsRecording())
{
changes.Add(change);
OnDirty?.Invoke();
}
switch (op)
{
case Operation.OP_ADD:
OnAdd?.Invoke(key);
OnChange?.Invoke(op, key, item);
break;
case Operation.OP_SET:
OnSet?.Invoke(key, oldItem, item);
OnChange?.Invoke(op, key, oldItem);
break;
case Operation.OP_UPDATE:
OnUpdate?.Invoke(key, oldItem);
OnChange?.Invoke(op, key, oldItem);
break;
case Operation.OP_REMOVE:
OnRemove?.Invoke(key, oldItem, item);
OnChange?.Invoke(op, key, oldItem);
break;
case Operation.OP_CLEAR:
OnClear?.Invoke();
OnChange?.Invoke(op, default, default);
break;
}
}
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => objects.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => objects.GetEnumerator();
}
public class ExtendedSyncDictionary<TKey, TValue> : ExtendedSyncIDictionary<TKey, TValue> where TValue : ISyncObjectUpdates<TValue>
{
public ExtendedSyncDictionary() : base(new Dictionary<TKey, TValue>()) { }
public ExtendedSyncDictionary(IEqualityComparer<TKey> eq) : base(new Dictionary<TKey, TValue>(eq)) { }
public ExtendedSyncDictionary(IDictionary<TKey, TValue> d) : base(new Dictionary<TKey, TValue>(d)) { }
public new Dictionary<TKey, TValue>.ValueCollection Values => ((Dictionary<TKey, TValue>)objects).Values;
public new Dictionary<TKey, TValue>.KeyCollection Keys => ((Dictionary<TKey, TValue>)objects).Keys;
public new Dictionary<TKey, TValue>.Enumerator GetEnumerator() => ((Dictionary<TKey, TValue>)objects).GetEnumerator();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment