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:
parent
c29b3daa0e
commit
8e7869b3da
|
|
@ -5,6 +5,7 @@ using System.Linq.Expressions;
|
|||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using AyCode.Core.Helpers;
|
||||
using AyCode.Core.Interfaces;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
|
|
@ -308,7 +309,27 @@ public static class AcJsonDeserializer
|
|||
}
|
||||
|
||||
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 (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)
|
||||
{
|
||||
var elementType = GetListElementType(listType);
|
||||
var elementType = GetCollectionElementType(listType);
|
||||
if (elementType == null) return;
|
||||
|
||||
targetList.Clear();
|
||||
|
||||
foreach (var item in arrayElement.EnumerateArray())
|
||||
// Use batch update for IAcObservableCollection to suppress per-item notifications
|
||||
var acObservable = targetList as IAcObservableCollection;
|
||||
acObservable?.BeginUpdate();
|
||||
|
||||
try
|
||||
{
|
||||
var value = ReadValue(item, elementType, context);
|
||||
if (value != null)
|
||||
targetList.Clear();
|
||||
|
||||
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)]
|
||||
|
|
@ -446,21 +478,55 @@ public static class AcJsonDeserializer
|
|||
var elementType = GetCollectionElementType(targetType);
|
||||
if (elementType == null) return null;
|
||||
|
||||
var list = GetOrCreateListFactory(elementType)();
|
||||
|
||||
foreach (var item in element.EnumerateArray())
|
||||
{
|
||||
list.Add(ReadValue(item, elementType, context));
|
||||
}
|
||||
|
||||
// For arrays, we need to use a temporary list
|
||||
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);
|
||||
list.CopyTo(array, 0);
|
||||
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)
|
||||
|
|
@ -472,47 +538,58 @@ public static class AcJsonDeserializer
|
|||
var existingList = (IList)existingCollection;
|
||||
var count = existingList.Count;
|
||||
|
||||
Dictionary<object, object>? existingById = null;
|
||||
if (count > 0)
|
||||
// Use batch update for IAcObservableCollection to suppress per-item notifications
|
||||
var acObservable = existingList as IAcObservableCollection;
|
||||
acObservable?.BeginUpdate();
|
||||
|
||||
try
|
||||
{
|
||||
existingById = new Dictionary<object, object>(count);
|
||||
for (var i = 0; i < count; i++)
|
||||
Dictionary<object, object>? existingById = null;
|
||||
if (count > 0)
|
||||
{
|
||||
var item = existingList[i];
|
||||
if (item != null)
|
||||
existingById = new Dictionary<object, object>(count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var id = idGetter(item);
|
||||
if (id != null && !IsDefaultId(id, idType))
|
||||
var item = existingList[i];
|
||||
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())
|
||||
{
|
||||
if (jsonItem.ValueKind != JsonValueKind.Object) continue;
|
||||
|
||||
object? itemId = null;
|
||||
if (jsonItem.TryGetProperty("Id", out var idProp))
|
||||
foreach (var jsonItem in arrayElement.EnumerateArray())
|
||||
{
|
||||
itemId = ReadPrimitive(idProp, idType, idProp.ValueKind);
|
||||
}
|
||||
if (jsonItem.ValueKind != JsonValueKind.Object) continue;
|
||||
|
||||
if (itemId != null && !IsDefaultId(itemId, idType) && existingById != null)
|
||||
{
|
||||
if (existingById.TryGetValue(itemId, out var existingItem))
|
||||
object? itemId = null;
|
||||
if (jsonItem.TryGetProperty("Id", out var idProp))
|
||||
{
|
||||
// Recursively merge nested objects
|
||||
var itemMetadata = GetTypeMetadata(elementType);
|
||||
PopulateObjectInternalMerge(jsonItem, existingItem, itemMetadata, context);
|
||||
continue;
|
||||
itemId = ReadPrimitive(idProp, idType, idProp.ValueKind);
|
||||
}
|
||||
|
||||
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);
|
||||
if (newItem != null) existingList.Add(newItem);
|
||||
}
|
||||
finally
|
||||
{
|
||||
acObservable?.EndUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1038,18 +1115,43 @@ public static class AcJsonDeserializer
|
|||
}
|
||||
|
||||
/// <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>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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();
|
||||
return genericDef == typeof(List<>) ||
|
||||
genericDef == typeof(IList<>) ||
|
||||
genericDef == typeof(ICollection<>) ||
|
||||
genericDef == typeof(IEnumerable<>);
|
||||
|
||||
// Check common collection types
|
||||
if (genericDef == typeof(List<>) ||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,23 +25,23 @@ namespace AyCode.Core.Helpers
|
|||
/// Fires a single Reset event at the end.
|
||||
/// </summary>
|
||||
void PopulateFromJson(string json, bool clearAll = false);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Begins a batch update operation. All notifications are suppressed until EndUpdate is called.
|
||||
/// Supports nested calls - only the outermost EndUpdate triggers the notification.
|
||||
/// </summary>
|
||||
public void BeginUpdate();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Ends a batch update operation. Triggers a single Reset notification if this is the outermost call.
|
||||
/// </summary>
|
||||
public void EndUpdate();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Forces a Reset notification to refresh bound UI controls.
|
||||
/// </summary>
|
||||
public void NotifyReset();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if currently in a batch update operation.
|
||||
/// </summary>
|
||||
|
|
@ -63,7 +63,7 @@ namespace AyCode.Core.Helpers
|
|||
{
|
||||
private readonly object _syncRoot = new();
|
||||
private int _updateCount;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if currently in a batch update operation.
|
||||
/// </summary>
|
||||
|
|
@ -109,7 +109,7 @@ namespace AyCode.Core.Helpers
|
|||
_updateCount--;
|
||||
shouldNotify = _updateCount == 0;
|
||||
}
|
||||
|
||||
|
||||
if (shouldNotify) NotifyReset();
|
||||
}
|
||||
|
||||
|
|
@ -227,7 +227,7 @@ namespace AyCode.Core.Helpers
|
|||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
return [..this.Items];
|
||||
return [.. this.Items];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -374,18 +374,30 @@ namespace AyCode.Core.Helpers
|
|||
|
||||
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
|
||||
{
|
||||
if (IsUpdating)
|
||||
return;
|
||||
if (IsUpdating) 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)
|
||||
{
|
||||
if (IsUpdating)
|
||||
return;
|
||||
if (IsUpdating) return;
|
||||
|
||||
base.OnCollectionChanged(e);
|
||||
try
|
||||
{
|
||||
base.OnCollectionChanged(e);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// A feliratkozott komponens már Disposed - biztonságosan figyelmen kívül hagyjuk
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -353,13 +353,9 @@ namespace AyCode.Services.Server.SignalRs
|
|||
{
|
||||
if (!setSourceToWorkingReferenceList)
|
||||
{
|
||||
if (InnerList is IAcObservableCollection observable)
|
||||
{
|
||||
observable.BeginUpdate();
|
||||
fromSource.CopyTo(InnerList);
|
||||
observable.EndUpdate();
|
||||
}
|
||||
else fromSource.CopyTo(InnerList);
|
||||
// CopyTo uses JSON serialization which already handles BeginUpdate/EndUpdate
|
||||
// for IAcObservableCollection via AcJsonDeserializer.Populate
|
||||
fromSource.CopyTo(InnerList);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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.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.SkipNegotiation = true; // Skip HTTP negotiation when using WebSockets only
|
||||
|
||||
//options.AccessTokenProvider = null;
|
||||
//options.HttpMessageHandlerFactory = null;
|
||||
|
|
@ -82,7 +83,7 @@ namespace AyCode.Services.SignalRs
|
|||
_ = HubConnection.On<int, byte[], int?>(nameof(IAcSignalRHubClient.OnReceiveMessage), OnReceiveMessage);
|
||||
//_ = HubConnection.On<int, int>(nameof(IAcSignalRHubClient.OnRequestMessage), OnRequestMessage);
|
||||
|
||||
HubConnection.StartAsync().Forget();
|
||||
//HubConnection.StartAsync().Forget();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Reference in New Issue