From 8e7869b3da8d47bedeb9cbdab7012000059c32bc Mon Sep 17 00:00:00 2001 From: Loretta Date: Thu, 11 Dec 2025 23:46:30 +0100 Subject: [PATCH] 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 and custom IList implementations. - Improve array and collection deserialization logic to better handle target types and fall back to List 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. --- AyCode.Core/Extensions/AcJsonDeserializer.cs | 202 +++++++++++++----- AyCode.Core/Helpers/AcObservableCollection.cs | 38 ++-- .../SignalRs/AcSignalRDataSource.cs | 10 +- .../SignalRs/AcSignalRClientBase.cs | 3 +- 4 files changed, 182 insertions(+), 71 deletions(-) diff --git a/AyCode.Core/Extensions/AcJsonDeserializer.cs b/AyCode.Core/Extensions/AcJsonDeserializer.cs index e3612d5..3e70a29 100644 --- a/AyCode.Core/Extensions/AcJsonDeserializer.cs +++ b/AyCode.Core/Extensions/AcJsonDeserializer.cs @@ -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, AcObservableCollection, etc. + IList? targetList = null; + try + { + var instance = Activator.CreateInstance(targetType); + if (instance is IList list) + { + targetList = list; + } + } + catch + { + // Fallback to List if we can't create the target type + } + + // Fallback to List 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? 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(count); - for (var i = 0; i < count; i++) + Dictionary? existingById = null; + if (count > 0) { - var item = existingList[i]; - if (item != null) + existingById = new Dictionary(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 } /// - /// Check if type is a generic collection type (List, IList, etc.) + /// Check if type is a generic collection type (List, IList, ObservableCollection, etc.) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsGenericCollectionType(Type type) { - if (!type.IsGenericType) return false; + if (!type.IsGenericType) + { + // Check if it implements IList interface (covers ObservableCollection 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 interface (covers AcObservableCollection and similar) + foreach (var iface in type.GetInterfaces()) + { + if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IList<>)) + return true; + } + + return false; } #endregion } diff --git a/AyCode.Core/Helpers/AcObservableCollection.cs b/AyCode.Core/Helpers/AcObservableCollection.cs index e2ab5bb..58cb117 100644 --- a/AyCode.Core/Helpers/AcObservableCollection.cs +++ b/AyCode.Core/Helpers/AcObservableCollection.cs @@ -25,23 +25,23 @@ namespace AyCode.Core.Helpers /// Fires a single Reset event at the end. /// void PopulateFromJson(string json, bool clearAll = false); - + /// /// Begins a batch update operation. All notifications are suppressed until EndUpdate is called. /// Supports nested calls - only the outermost EndUpdate triggers the notification. /// public void BeginUpdate(); - + /// /// Ends a batch update operation. Triggers a single Reset notification if this is the outermost call. /// public void EndUpdate(); - + /// /// Forces a Reset notification to refresh bound UI controls. /// public void NotifyReset(); - + /// /// Returns true if currently in a batch update operation. /// @@ -63,7 +63,7 @@ namespace AyCode.Core.Helpers { private readonly object _syncRoot = new(); private int _updateCount; - + /// /// Returns true if currently in a batch update operation. /// @@ -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 + } } } } \ No newline at end of file diff --git a/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs b/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs index a20ab74..e7e03f5 100644 --- a/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs +++ b/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs @@ -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 { diff --git a/AyCode.Services/SignalRs/AcSignalRClientBase.cs b/AyCode.Services/SignalRs/AcSignalRClientBase.cs index e41ce6a..3a7abd5 100644 --- a/AyCode.Services/SignalRs/AcSignalRClientBase.cs +++ b/AyCode.Services/SignalRs/AcSignalRClientBase.cs @@ -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(nameof(IAcSignalRHubClient.OnReceiveMessage), OnReceiveMessage); //_ = HubConnection.On(nameof(IAcSignalRHubClient.OnRequestMessage), OnRequestMessage); - HubConnection.StartAsync().Forget(); + //HubConnection.StartAsync().Forget(); } ///