Improve JSON deserialization and observable collection handling

- Add batch update support for IAcObservableCollection during deserialization to suppress per-item notifications and fire a single reset event.
- Throw AcJsonDeserializationException with a clear message if a type lacks a parameterless constructor during deserialization.
- Enhance IsGenericCollectionType to recognize more collection types, including ObservableCollection<T> and custom IList<T> implementations.
- Improve array and collection deserialization logic to better handle target types and fall back to List<T> if needed.
- In AcObservableCollection, catch and ignore ObjectDisposedException in event handlers to prevent errors from disposed subscribers.
- Remove redundant batch update logic from AcSignalRDataSource; rely on deserializer's handling.
- Set SkipNegotiation = true in SignalR client options for WebSockets-only optimization and comment out automatic HubConnection.StartAsync() for more controlled connection management.
This commit is contained in:
Loretta 2025-12-11 23:46:30 +01:00
parent c29b3daa0e
commit 8e7869b3da
4 changed files with 182 additions and 71 deletions

View File

@ -5,6 +5,7 @@ using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.Json; using System.Text.Json;
using AyCode.Core.Helpers;
using AyCode.Core.Interfaces; using AyCode.Core.Interfaces;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -308,7 +309,27 @@ public static class AcJsonDeserializer
} }
var metadata = GetTypeMetadata(targetType); var metadata = GetTypeMetadata(targetType);
var instance = metadata.CompiledConstructor?.Invoke() ?? Activator.CreateInstance(targetType);
object? instance;
if (metadata.CompiledConstructor != null)
{
instance = metadata.CompiledConstructor.Invoke();
}
else
{
try
{
instance = Activator.CreateInstance(targetType);
}
catch (MissingMethodException ex)
{
throw new AcJsonDeserializationException(
$"Cannot deserialize type '{targetType.FullName}' because it does not have a parameterless constructor. " +
$"Add a parameterless constructor or use a different serialization approach.",
null, targetType, ex);
}
}
if (instance == null) return null; if (instance == null) return null;
if (element.TryGetProperty("$id", out var idElement)) if (element.TryGetProperty("$id", out var idElement))
@ -425,19 +446,30 @@ public static class AcJsonDeserializer
private static void PopulateList(JsonElement arrayElement, IList targetList, Type listType, DeserializationContext context) private static void PopulateList(JsonElement arrayElement, IList targetList, Type listType, DeserializationContext context)
{ {
var elementType = GetListElementType(listType); var elementType = GetCollectionElementType(listType);
if (elementType == null) return; if (elementType == null) return;
targetList.Clear(); // Use batch update for IAcObservableCollection to suppress per-item notifications
var acObservable = targetList as IAcObservableCollection;
foreach (var item in arrayElement.EnumerateArray()) acObservable?.BeginUpdate();
try
{ {
var value = ReadValue(item, elementType, context); targetList.Clear();
if (value != null)
foreach (var item in arrayElement.EnumerateArray())
{ {
targetList.Add(value); var value = ReadValue(item, elementType, context);
if (value != null)
{
targetList.Add(value);
}
} }
} }
finally
{
acObservable?.EndUpdate();
}
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -446,21 +478,55 @@ public static class AcJsonDeserializer
var elementType = GetCollectionElementType(targetType); var elementType = GetCollectionElementType(targetType);
if (elementType == null) return null; if (elementType == null) return null;
var list = GetOrCreateListFactory(elementType)(); // For arrays, we need to use a temporary list
foreach (var item in element.EnumerateArray())
{
list.Add(ReadValue(item, elementType, context));
}
if (targetType.IsArray) if (targetType.IsArray)
{ {
var list = GetOrCreateListFactory(elementType)();
foreach (var item in element.EnumerateArray())
{
list.Add(ReadValue(item, elementType, context));
}
var array = Array.CreateInstance(elementType, list.Count); var array = Array.CreateInstance(elementType, list.Count);
list.CopyTo(array, 0); list.CopyTo(array, 0);
return array; return array;
} }
return list; // Try to create an instance of the target collection type directly
// This handles ObservableCollection<T>, AcObservableCollection<T>, etc.
IList? targetList = null;
try
{
var instance = Activator.CreateInstance(targetType);
if (instance is IList list)
{
targetList = list;
}
}
catch
{
// Fallback to List<T> if we can't create the target type
}
// Fallback to List<T> if target type couldn't be instantiated
targetList ??= GetOrCreateListFactory(elementType)();
// Use batch update for IAcObservableCollection to suppress per-item notifications
var acObservable = targetList as IAcObservableCollection;
acObservable?.BeginUpdate();
try
{
foreach (var item in element.EnumerateArray())
{
targetList.Add(ReadValue(item, elementType, context));
}
}
finally
{
acObservable?.EndUpdate();
}
return targetList;
} }
private static void MergeIIdCollection(JsonElement arrayElement, object existingCollection, PropertySetterInfo propInfo, DeserializationContext context) private static void MergeIIdCollection(JsonElement arrayElement, object existingCollection, PropertySetterInfo propInfo, DeserializationContext context)
@ -472,47 +538,58 @@ public static class AcJsonDeserializer
var existingList = (IList)existingCollection; var existingList = (IList)existingCollection;
var count = existingList.Count; var count = existingList.Count;
Dictionary<object, object>? existingById = null; // Use batch update for IAcObservableCollection to suppress per-item notifications
if (count > 0) var acObservable = existingList as IAcObservableCollection;
acObservable?.BeginUpdate();
try
{ {
existingById = new Dictionary<object, object>(count); Dictionary<object, object>? existingById = null;
for (var i = 0; i < count; i++) if (count > 0)
{ {
var item = existingList[i]; existingById = new Dictionary<object, object>(count);
if (item != null) for (var i = 0; i < count; i++)
{ {
var id = idGetter(item); var item = existingList[i];
if (id != null && !IsDefaultId(id, idType)) if (item != null)
{ {
existingById[id] = item; var id = idGetter(item);
if (id != null && !IsDefaultId(id, idType))
{
existingById[id] = item;
}
} }
} }
} }
}
foreach (var jsonItem in arrayElement.EnumerateArray()) foreach (var jsonItem in arrayElement.EnumerateArray())
{
if (jsonItem.ValueKind != JsonValueKind.Object) continue;
object? itemId = null;
if (jsonItem.TryGetProperty("Id", out var idProp))
{ {
itemId = ReadPrimitive(idProp, idType, idProp.ValueKind); if (jsonItem.ValueKind != JsonValueKind.Object) continue;
}
if (itemId != null && !IsDefaultId(itemId, idType) && existingById != null) object? itemId = null;
{ if (jsonItem.TryGetProperty("Id", out var idProp))
if (existingById.TryGetValue(itemId, out var existingItem))
{ {
// Recursively merge nested objects itemId = ReadPrimitive(idProp, idType, idProp.ValueKind);
var itemMetadata = GetTypeMetadata(elementType);
PopulateObjectInternalMerge(jsonItem, existingItem, itemMetadata, context);
continue;
} }
if (itemId != null && !IsDefaultId(itemId, idType) && existingById != null)
{
if (existingById.TryGetValue(itemId, out var existingItem))
{
// Recursively merge nested objects
var itemMetadata = GetTypeMetadata(elementType);
PopulateObjectInternalMerge(jsonItem, existingItem, itemMetadata, context);
continue;
}
}
var newItem = ReadValue(jsonItem, elementType, context);
if (newItem != null) existingList.Add(newItem);
} }
}
var newItem = ReadValue(jsonItem, elementType, context); finally
if (newItem != null) existingList.Add(newItem); {
acObservable?.EndUpdate();
} }
} }
@ -1038,18 +1115,43 @@ public static class AcJsonDeserializer
} }
/// <summary> /// <summary>
/// Check if type is a generic collection type (List, IList, etc.) /// Check if type is a generic collection type (List, IList, ObservableCollection, etc.)
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsGenericCollectionType(Type type) private static bool IsGenericCollectionType(Type type)
{ {
if (!type.IsGenericType) return false; if (!type.IsGenericType)
{
// Check if it implements IList<T> interface (covers ObservableCollection<T> subclasses)
foreach (var iface in type.GetInterfaces())
{
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IList<>))
return true;
}
return false;
}
var genericDef = type.GetGenericTypeDefinition(); var genericDef = type.GetGenericTypeDefinition();
return genericDef == typeof(List<>) ||
genericDef == typeof(IList<>) || // Check common collection types
genericDef == typeof(ICollection<>) || if (genericDef == typeof(List<>) ||
genericDef == typeof(IEnumerable<>); genericDef == typeof(IList<>) ||
genericDef == typeof(ICollection<>) ||
genericDef == typeof(IEnumerable<>) ||
genericDef == typeof(System.Collections.ObjectModel.ObservableCollection<>) ||
genericDef == typeof(System.Collections.ObjectModel.Collection<>))
{
return true;
}
// Check if it implements IList<T> interface (covers AcObservableCollection<T> and similar)
foreach (var iface in type.GetInterfaces())
{
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IList<>))
return true;
}
return false;
} }
#endregion #endregion
} }

View File

@ -25,23 +25,23 @@ namespace AyCode.Core.Helpers
/// Fires a single Reset event at the end. /// Fires a single Reset event at the end.
/// </summary> /// </summary>
void PopulateFromJson(string json, bool clearAll = false); void PopulateFromJson(string json, bool clearAll = false);
/// <summary> /// <summary>
/// Begins a batch update operation. All notifications are suppressed until EndUpdate is called. /// Begins a batch update operation. All notifications are suppressed until EndUpdate is called.
/// Supports nested calls - only the outermost EndUpdate triggers the notification. /// Supports nested calls - only the outermost EndUpdate triggers the notification.
/// </summary> /// </summary>
public void BeginUpdate(); public void BeginUpdate();
/// <summary> /// <summary>
/// Ends a batch update operation. Triggers a single Reset notification if this is the outermost call. /// Ends a batch update operation. Triggers a single Reset notification if this is the outermost call.
/// </summary> /// </summary>
public void EndUpdate(); public void EndUpdate();
/// <summary> /// <summary>
/// Forces a Reset notification to refresh bound UI controls. /// Forces a Reset notification to refresh bound UI controls.
/// </summary> /// </summary>
public void NotifyReset(); public void NotifyReset();
/// <summary> /// <summary>
/// Returns true if currently in a batch update operation. /// Returns true if currently in a batch update operation.
/// </summary> /// </summary>
@ -63,7 +63,7 @@ namespace AyCode.Core.Helpers
{ {
private readonly object _syncRoot = new(); private readonly object _syncRoot = new();
private int _updateCount; private int _updateCount;
/// <summary> /// <summary>
/// Returns true if currently in a batch update operation. /// Returns true if currently in a batch update operation.
/// </summary> /// </summary>
@ -109,7 +109,7 @@ namespace AyCode.Core.Helpers
_updateCount--; _updateCount--;
shouldNotify = _updateCount == 0; shouldNotify = _updateCount == 0;
} }
if (shouldNotify) NotifyReset(); if (shouldNotify) NotifyReset();
} }
@ -227,7 +227,7 @@ namespace AyCode.Core.Helpers
{ {
lock (_syncRoot) lock (_syncRoot)
{ {
return [..this.Items]; return [.. this.Items];
} }
} }
@ -374,18 +374,30 @@ namespace AyCode.Core.Helpers
protected override void OnPropertyChanged(PropertyChangedEventArgs e) protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{ {
if (IsUpdating) if (IsUpdating) return;
return;
base.OnPropertyChanged(e); try
{
base.OnPropertyChanged(e);
}
catch (ObjectDisposedException)
{
// A feliratkozott komponens már Disposed - biztonságosan figyelmen kívül hagyjuk
}
} }
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{ {
if (IsUpdating) if (IsUpdating) return;
return;
base.OnCollectionChanged(e); try
{
base.OnCollectionChanged(e);
}
catch (ObjectDisposedException)
{
// A feliratkozott komponens már Disposed - biztonságosan figyelmen kívül hagyjuk
}
} }
} }
} }

View File

@ -353,13 +353,9 @@ namespace AyCode.Services.Server.SignalRs
{ {
if (!setSourceToWorkingReferenceList) if (!setSourceToWorkingReferenceList)
{ {
if (InnerList is IAcObservableCollection observable) // CopyTo uses JSON serialization which already handles BeginUpdate/EndUpdate
{ // for IAcObservableCollection via AcJsonDeserializer.Populate
observable.BeginUpdate(); fromSource.CopyTo(InnerList);
fromSource.CopyTo(InnerList);
observable.EndUpdate();
}
else fromSource.CopyTo(InnerList);
} }
else else
{ {

View File

@ -46,6 +46,7 @@ namespace AyCode.Services.SignalRs
options.TransportMaxBufferSize = 30_000_000; //Increasing this value allows the client to receive larger messages. default: 65KB; unlimited: 0;; options.TransportMaxBufferSize = 30_000_000; //Increasing this value allows the client to receive larger messages. default: 65KB; unlimited: 0;;
options.ApplicationMaxBufferSize = 30_000_000; //Increasing this value allows the client to send larger messages. default: 65KB; unlimited: 0; options.ApplicationMaxBufferSize = 30_000_000; //Increasing this value allows the client to send larger messages. default: 65KB; unlimited: 0;
options.CloseTimeout = TimeSpan.FromSeconds(10); //default: 5 sec. options.CloseTimeout = TimeSpan.FromSeconds(10); //default: 5 sec.
options.SkipNegotiation = true; // Skip HTTP negotiation when using WebSockets only
//options.AccessTokenProvider = null; //options.AccessTokenProvider = null;
//options.HttpMessageHandlerFactory = null; //options.HttpMessageHandlerFactory = null;
@ -82,7 +83,7 @@ namespace AyCode.Services.SignalRs
_ = HubConnection.On<int, byte[], int?>(nameof(IAcSignalRHubClient.OnReceiveMessage), OnReceiveMessage); _ = HubConnection.On<int, byte[], int?>(nameof(IAcSignalRHubClient.OnReceiveMessage), OnReceiveMessage);
//_ = HubConnection.On<int, int>(nameof(IAcSignalRHubClient.OnRequestMessage), OnRequestMessage); //_ = HubConnection.On<int, int>(nameof(IAcSignalRHubClient.OnRequestMessage), OnRequestMessage);
HubConnection.StartAsync().Forget(); //HubConnection.StartAsync().Forget();
} }
/// <summary> /// <summary>