Refactor fullscreen grid UI; add serializer diagnostics/tests
- Replaced DxWindow with custom Bootstrap 5 fullscreen overlay for grid components, improving fullscreen UX and styling. - Added new CSS for fullscreen overlay, header, and body; retained legacy DxWindow styles for compatibility. - Introduced SignalRSerializerDiagnosticLog flag to control binary serializer diagnostics at runtime. - Enabled diagnostics in DevAdminSignalRHub, FruitBankSignalRClient, and Program.cs based on the new flag. - Updated OrderClientTests to use GetStockTakings(false). - Added StockTakingSerializerTests for binary serialization/deserialization validation and debugging.
This commit is contained in:
parent
3c5b737207
commit
d27f53453f
|
|
@ -35,6 +35,7 @@ public class DevAdminSignalRHub : AcWebSignalRHubWithSessionBase<SignalRTags, Lo
|
||||||
ICustomOrderSignalREndpointServer customOrderSignalREndpoint, IStockSignalREndpointServer stockSignalREndpointServer, IEnumerable<IAcLogWriterBase> logWriters)
|
ICustomOrderSignalREndpointServer customOrderSignalREndpoint, IStockSignalREndpointServer stockSignalREndpointServer, IEnumerable<IAcLogWriterBase> logWriters)
|
||||||
: base(configuration, new Logger<DevAdminSignalRHub>(logWriters.ToArray()))
|
: base(configuration, new Logger<DevAdminSignalRHub>(logWriters.ToArray()))
|
||||||
{
|
{
|
||||||
|
EnableBinaryDiagnostics = FruitBankConstClient.SignalRSerializerDiagnosticLog;
|
||||||
SerializerOptions = new AcBinarySerializerOptions();
|
SerializerOptions = new AcBinarySerializerOptions();
|
||||||
//SerializerOptions = new AcJsonSerializerOptions();
|
//SerializerOptions = new AcJsonSerializerOptions();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ public static class FruitBankConstClient
|
||||||
public static string DefaultHubName = "fbHub";
|
public static string DefaultHubName = "fbHub";
|
||||||
public static string LoggerHubName = "loggerHub";
|
public static string LoggerHubName = "loggerHub";
|
||||||
|
|
||||||
|
public static bool SignalRSerializerDiagnosticLog = false;
|
||||||
public static long SignalRKeepAliveIntervalSecond = 60;
|
public static long SignalRKeepAliveIntervalSecond = 60;
|
||||||
public static long SignarlRTimeoutIntervalSecond = 180;
|
public static long SignarlRTimeoutIntervalSecond = 180;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ public sealed class OrderClientTests
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public async Task GetAllStockTakings()
|
public async Task GetAllStockTakings()
|
||||||
{
|
{
|
||||||
var stockTakings = await _signalRClient.GetStockTakings(true);
|
var stockTakings = await _signalRClient.GetStockTakings(false);
|
||||||
|
|
||||||
Assert.IsNotNull(stockTakings);
|
Assert.IsNotNull(stockTakings);
|
||||||
Assert.IsTrue(stockTakings.Count != 0);
|
Assert.IsTrue(stockTakings.Count != 0);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,267 @@
|
||||||
|
using AyCode.Core.Extensions;
|
||||||
|
using AyCode.Core.Serializers.Binaries;
|
||||||
|
using FruitBank.Common.Entities;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace FruitBankHybrid.Shared.Tests;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class StockTakingSerializerTests
|
||||||
|
{
|
||||||
|
[TestMethod]
|
||||||
|
public void StockTaking_WithNullItems_RoundTrip()
|
||||||
|
{
|
||||||
|
var stockTaking = new StockTaking
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc),
|
||||||
|
IsClosed = false,
|
||||||
|
Creator = 6,
|
||||||
|
Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
|
||||||
|
Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc),
|
||||||
|
StockTakingItems = null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Log StockTaking properties
|
||||||
|
Console.WriteLine("=== StockTaking Properties ===");
|
||||||
|
var stProps = typeof(StockTaking).GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||||
|
.Where(p => p.CanRead && p.GetIndexParameters().Length == 0)
|
||||||
|
.ToArray();
|
||||||
|
for (int i = 0; i < stProps.Length; i++)
|
||||||
|
Console.WriteLine($" [{i}] {stProps[i].Name} : {stProps[i].PropertyType.Name} (from: {stProps[i].DeclaringType?.Name})");
|
||||||
|
|
||||||
|
// Log StockTakingItem properties
|
||||||
|
Console.WriteLine("\n=== StockTakingItem Properties ===");
|
||||||
|
var stiProps = typeof(StockTakingItem).GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||||
|
.Where(p => p.CanRead && p.GetIndexParameters().Length == 0)
|
||||||
|
.ToArray();
|
||||||
|
for (int i = 0; i < stiProps.Length; i++)
|
||||||
|
{
|
||||||
|
var hasIgnore = Attribute.IsDefined(stiProps[i], typeof(Newtonsoft.Json.JsonIgnoreAttribute)) ||
|
||||||
|
Attribute.IsDefined(stiProps[i], typeof(System.Text.Json.Serialization.JsonIgnoreAttribute));
|
||||||
|
Console.WriteLine($" [{i}] {stiProps[i].Name} : {stiProps[i].PropertyType.Name}{(hasIgnore ? " [IGNORED]" : "")}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var binary = stockTaking.ToBinary();
|
||||||
|
Console.WriteLine($"\nBinary length: {binary.Length}");
|
||||||
|
|
||||||
|
// Parse header
|
||||||
|
var pos = 0;
|
||||||
|
var version = binary[pos++];
|
||||||
|
var marker = binary[pos++];
|
||||||
|
Console.WriteLine($"Version: {version}, Marker: 0x{marker:X2}");
|
||||||
|
|
||||||
|
if ((marker & 0x10) != 0)
|
||||||
|
{
|
||||||
|
var propCount = binary[pos++];
|
||||||
|
Console.WriteLine($"\n=== HEADER ({propCount} properties) ===");
|
||||||
|
for (int i = 0; i < propCount; i++)
|
||||||
|
{
|
||||||
|
var strLen = binary[pos++];
|
||||||
|
var propName = System.Text.Encoding.UTF8.GetString(binary, pos, strLen);
|
||||||
|
pos += strLen;
|
||||||
|
Console.WriteLine($" [{i}]: '{propName}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = binary.BinaryTo<StockTaking>();
|
||||||
|
|
||||||
|
Console.WriteLine($"\n=== RESULT ===");
|
||||||
|
Console.WriteLine($"Id: {result?.Id}");
|
||||||
|
Console.WriteLine($"Creator: {result?.Creator}");
|
||||||
|
Console.WriteLine($"Created: {result?.Created}");
|
||||||
|
|
||||||
|
Assert.IsNotNull(result);
|
||||||
|
Assert.AreEqual(1, result.Id, "Id mismatch");
|
||||||
|
Assert.AreEqual(6, result.Creator, $"Creator should be 6, got {result.Creator}");
|
||||||
|
Assert.AreEqual(stockTaking.Created, result.Created, $"Created mismatch");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"\n=== ERROR ===");
|
||||||
|
Console.WriteLine(ex.Message);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ListOfStockTaking_WithNullItems_RoundTrip()
|
||||||
|
{
|
||||||
|
var stockTakings = new List<StockTaking>
|
||||||
|
{
|
||||||
|
new() { Id = 1, StartDateTime = DateTime.UtcNow, IsClosed = false, Creator = 6, Created = DateTime.UtcNow, Modified = DateTime.UtcNow, StockTakingItems = null },
|
||||||
|
new() { Id = 2, StartDateTime = DateTime.UtcNow, IsClosed = true, Creator = 12, Created = DateTime.UtcNow, Modified = DateTime.UtcNow, StockTakingItems = null }
|
||||||
|
};
|
||||||
|
|
||||||
|
var binary = stockTakings.ToBinary();
|
||||||
|
Console.WriteLine($"Binary length: {binary.Length}");
|
||||||
|
|
||||||
|
var result = binary.BinaryTo<List<StockTaking>>();
|
||||||
|
|
||||||
|
Assert.IsNotNull(result);
|
||||||
|
Assert.AreEqual(2, result.Count);
|
||||||
|
Assert.AreEqual(6, result[0].Creator, $"First Creator should be 6, got {result[0].Creator}");
|
||||||
|
Assert.AreEqual(12, result[1].Creator, $"Second Creator should be 12, got {result[1].Creator}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void StockTaking_DebugBodyProperties()
|
||||||
|
{
|
||||||
|
var stockTaking = new StockTaking
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc),
|
||||||
|
IsClosed = false,
|
||||||
|
Creator = 6,
|
||||||
|
Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
|
||||||
|
Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc),
|
||||||
|
StockTakingItems = null
|
||||||
|
};
|
||||||
|
|
||||||
|
var binary = stockTaking.ToBinary();
|
||||||
|
|
||||||
|
// Parse header
|
||||||
|
var pos = 0;
|
||||||
|
var version = binary[pos++];
|
||||||
|
var marker = binary[pos++];
|
||||||
|
|
||||||
|
var propertyNames = new List<string>();
|
||||||
|
if ((marker & 0x10) != 0)
|
||||||
|
{
|
||||||
|
var propCount = (int)binary[pos++]; // Simplified VarUInt read
|
||||||
|
for (int i = 0; i < propCount; i++)
|
||||||
|
{
|
||||||
|
var strLen = (int)binary[pos++];
|
||||||
|
var propName = System.Text.Encoding.UTF8.GetString(binary, pos, strLen);
|
||||||
|
pos += strLen;
|
||||||
|
propertyNames.Add(propName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse body
|
||||||
|
Console.WriteLine($"\n=== BODY (starts at position {pos}) ===");
|
||||||
|
var objectMarker = binary[pos++];
|
||||||
|
Console.WriteLine($"Object marker: 0x{objectMarker:X2}");
|
||||||
|
|
||||||
|
// Read ref ID (VarInt)
|
||||||
|
var refIdByte = binary[pos];
|
||||||
|
if ((refIdByte & 0x80) == 0) pos++;
|
||||||
|
else pos += 2;
|
||||||
|
|
||||||
|
// Read property count
|
||||||
|
var bodyPropCount = binary[pos++];
|
||||||
|
Console.WriteLine($"Body property count: {bodyPropCount}");
|
||||||
|
|
||||||
|
for (int i = 0; i < bodyPropCount; i++)
|
||||||
|
{
|
||||||
|
var propIndex = binary[pos++];
|
||||||
|
var propName = propIndex < propertyNames.Count ? propertyNames[propIndex] : $"UNKNOWN({propIndex})";
|
||||||
|
var valueType = binary[pos];
|
||||||
|
|
||||||
|
string valueInfo;
|
||||||
|
if (valueType == 0x14) // DateTime
|
||||||
|
{
|
||||||
|
valueInfo = "DateTime";
|
||||||
|
pos += 10;
|
||||||
|
}
|
||||||
|
else if (valueType >= 0xD0) // TinyInt
|
||||||
|
{
|
||||||
|
var tinyValue = valueType - 0xD0;
|
||||||
|
valueInfo = $"TinyInt({tinyValue})";
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
else if (valueType == 0x03)
|
||||||
|
{
|
||||||
|
valueInfo = "False";
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
else if (valueType == 0x02)
|
||||||
|
{
|
||||||
|
valueInfo = "True";
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
valueInfo = $"0x{valueType:X2}";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($" Body[{i}]: index={propIndex} -> '{propName}' = {valueInfo}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void StockTaking_ExactProductionData_RoundTrip()
|
||||||
|
{
|
||||||
|
var stockTakings = new List<StockTaking>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 7,
|
||||||
|
StartDateTime = new DateTime(2025, 12, 3, 8, 55, 43, 539, DateTimeKind.Utc),
|
||||||
|
IsClosed = false,
|
||||||
|
Creator = 6,
|
||||||
|
Created = new DateTime(2025, 12, 3, 7, 55, 43, 571, DateTimeKind.Utc),
|
||||||
|
Modified = new DateTime(2025, 12, 3, 7, 55, 43, 571, DateTimeKind.Utc),
|
||||||
|
StockTakingItems = null
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 6,
|
||||||
|
StartDateTime = new DateTime(2025, 12, 2, 8, 21, 26, 439, DateTimeKind.Utc),
|
||||||
|
IsClosed = true,
|
||||||
|
Creator = 6,
|
||||||
|
Created = new DateTime(2025, 12, 2, 7, 21, 26, 468, DateTimeKind.Utc),
|
||||||
|
Modified = new DateTime(2025, 12, 2, 7, 21, 26, 468, DateTimeKind.Utc),
|
||||||
|
StockTakingItems = null
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 3,
|
||||||
|
StartDateTime = new DateTime(2025, 11, 30, 14, 1, 55, 663, DateTimeKind.Utc),
|
||||||
|
IsClosed = true,
|
||||||
|
Creator = 6,
|
||||||
|
Created = new DateTime(2025, 11, 30, 13, 1, 55, 692, DateTimeKind.Utc),
|
||||||
|
Modified = new DateTime(2025, 11, 30, 13, 1, 55, 692, DateTimeKind.Utc),
|
||||||
|
StockTakingItems = null
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
StartDateTime = new DateTime(2025, 11, 30, 8, 20, 2, 182, DateTimeKind.Utc),
|
||||||
|
IsClosed = true,
|
||||||
|
Creator = 6,
|
||||||
|
Created = new DateTime(2025, 11, 30, 7, 20, 3, 331, DateTimeKind.Utc),
|
||||||
|
Modified = new DateTime(2025, 11, 30, 7, 20, 3, 331, DateTimeKind.Utc),
|
||||||
|
StockTakingItems = null
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
StartDateTime = new DateTime(2025, 11, 30, 8, 18, 59, 693, DateTimeKind.Utc),
|
||||||
|
IsClosed = true,
|
||||||
|
Creator = 6,
|
||||||
|
Created = new DateTime(2025, 11, 30, 7, 19, 1, 849, DateTimeKind.Utc),
|
||||||
|
Modified = new DateTime(2025, 11, 30, 7, 19, 1, 877, DateTimeKind.Utc),
|
||||||
|
StockTakingItems = null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var binary = stockTakings.ToBinary();
|
||||||
|
Console.WriteLine($"Binary length: {binary.Length}");
|
||||||
|
|
||||||
|
var result = binary.BinaryTo<List<StockTaking>>();
|
||||||
|
|
||||||
|
Assert.IsNotNull(result);
|
||||||
|
Assert.AreEqual(5, result.Count);
|
||||||
|
|
||||||
|
foreach (var item in result)
|
||||||
|
{
|
||||||
|
Assert.AreEqual(6, item.Creator,
|
||||||
|
$"StockTaking Id={item.Id}: Creator should be 6, got {item.Creator}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -33,7 +33,7 @@ namespace FruitBankHybrid.Shared.Services.SignalRs
|
||||||
// .WithStatefulReconnect()
|
// .WithStatefulReconnect()
|
||||||
// .WithKeepAliveInterval(TimeSpan.FromSeconds(60))
|
// .WithKeepAliveInterval(TimeSpan.FromSeconds(60))
|
||||||
// .WithServerTimeout(TimeSpan.FromSeconds(120))
|
// .WithServerTimeout(TimeSpan.FromSeconds(120))
|
||||||
|
EnableBinaryDiagnostics = FruitBankConstClient.SignalRSerializerDiagnosticLog;
|
||||||
ConstHelper.NameByValue<SignalRTags>(0);
|
ConstHelper.NameByValue<SignalRTags>(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
using AyCode.Core.Loggers;
|
using AyCode.Core.Loggers;
|
||||||
|
using AyCode.Services.SignalRs;
|
||||||
|
using FruitBank.Common;
|
||||||
using FruitBank.Common.Loggers;
|
using FruitBank.Common.Loggers;
|
||||||
using FruitBank.Common.Models;
|
using FruitBank.Common.Models;
|
||||||
using FruitBank.Common.Services;
|
using FruitBank.Common.Services;
|
||||||
|
|
@ -29,4 +31,11 @@ builder.Services.AddSingleton<DatabaseClient>();
|
||||||
|
|
||||||
builder.Services.AddSingleton<IAcLogWriterClientBase, SignaRClientLogItemWriter>();
|
builder.Services.AddSingleton<IAcLogWriterClientBase, SignaRClientLogItemWriter>();
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
if (FruitBankConstClient.SignalRSerializerDiagnosticLog)
|
||||||
|
{
|
||||||
|
SignalResponseDataMessage.DiagnosticLogger = message => { Console.WriteLine(message); };
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
await builder.Build().RunAsync();
|
await builder.Build().RunAsync();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue