From 0a54fa5e35cb3526a2b620ce83ba78e2d9e781fc Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 21 May 2026 14:34:21 -0400 Subject: [PATCH] Render array values and element type in the Browse panel The subscription panel showed "(array)" for any array-valued tag and "Unspecified" for its type. A scalar MxValue carries its type in MxValue.DataType, but an array leaves that Unspecified and carries the element type and dimensions on the MxArray itself. DashboardMxValueFormatter.FormatValue now joins the typed MxArray elements (e.g. "[1.5, 2.25, 3]"), capped at 24 elements with a "... N total" suffix, and FormatDataType reads the element type and dimensions off the array (e.g. "Double[10]"). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Dashboard/DashboardMxValueFormatter.cs | 63 ++++++++++++++++++- .../DashboardBrowseAndAlarmModelTests.cs | 13 ++++ 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/MxGateway.Server/Dashboard/DashboardMxValueFormatter.cs b/src/MxGateway.Server/Dashboard/DashboardMxValueFormatter.cs index 545fdea..1fde4c1 100644 --- a/src/MxGateway.Server/Dashboard/DashboardMxValueFormatter.cs +++ b/src/MxGateway.Server/Dashboard/DashboardMxValueFormatter.cs @@ -10,6 +10,9 @@ namespace MxGateway.Server.Dashboard; /// public static class DashboardMxValueFormatter { + /// Maximum array elements rendered inline before the value is truncated. + private const int MaxArrayElements = 24; + /// Formats the value payload of an . /// The value to format; may be null. /// A display string — never null. @@ -37,7 +40,7 @@ public static class DashboardMxValueFormatter .ToDateTimeOffset() .UtcDateTime .ToString("yyyy-MM-dd HH:mm:ss.fff 'UTC'", CultureInfo.InvariantCulture), - MxValue.KindOneofCase.ArrayValue => "(array)", + MxValue.KindOneofCase.ArrayValue => FormatArray(value.ArrayValue), MxValue.KindOneofCase.RawValue => $"({value.RawValue.Length} bytes)", _ => "-", }; @@ -45,9 +48,63 @@ public static class DashboardMxValueFormatter /// Formats the MXAccess data type of an . /// The value whose data type to describe; may be null. - /// The data-type name — never null. + /// The data-type name — never null. Arrays render as Element[dims]. public static string FormatDataType(MxValue? value) { - return value is null ? "-" : value.DataType.ToString(); + if (value is null) + { + return "-"; + } + + // A scalar carries its type in MxValue.DataType, but an array leaves + // that Unspecified and carries the element type on the MxArray itself. + return value.KindCase == MxValue.KindOneofCase.ArrayValue + ? FormatArrayDataType(value.ArrayValue) + : value.DataType.ToString(); + } + + private static string FormatArrayDataType(MxArray array) + { + string dimensions = array.Dimensions.Count > 0 + ? string.Join(",", array.Dimensions) + : string.Empty; + return $"{array.ElementDataType}[{dimensions}]"; + } + + private static string FormatArray(MxArray array) + { + IReadOnlyList elements = array.ValuesCase switch + { + MxArray.ValuesOneofCase.BoolValues => + array.BoolValues.Values.Select(item => item ? "true" : "false").ToArray(), + MxArray.ValuesOneofCase.Int32Values => + array.Int32Values.Values.Select(item => item.ToString(CultureInfo.InvariantCulture)).ToArray(), + MxArray.ValuesOneofCase.Int64Values => + array.Int64Values.Values.Select(item => item.ToString(CultureInfo.InvariantCulture)).ToArray(), + MxArray.ValuesOneofCase.FloatValues => + array.FloatValues.Values.Select(item => item.ToString("G7", CultureInfo.InvariantCulture)).ToArray(), + MxArray.ValuesOneofCase.DoubleValues => + array.DoubleValues.Values.Select(item => item.ToString("G15", CultureInfo.InvariantCulture)).ToArray(), + MxArray.ValuesOneofCase.StringValues => + array.StringValues.Values.Select(item => $"\"{item}\"").ToArray(), + MxArray.ValuesOneofCase.TimestampValues => + array.TimestampValues.Values + .Select(item => item.ToDateTimeOffset().UtcDateTime + .ToString("yyyy-MM-dd HH:mm:ss 'UTC'", CultureInfo.InvariantCulture)) + .ToArray(), + MxArray.ValuesOneofCase.RawValues => + array.RawValues.Values.Select(item => $"({item.Length} bytes)").ToArray(), + _ => [], + }; + + if (elements.Count == 0) + { + return "[]"; + } + + string body = string.Join(", ", elements.Take(MaxArrayElements)); + return elements.Count > MaxArrayElements + ? $"[{body}, … {elements.Count} total]" + : $"[{body}]"; } } diff --git a/src/MxGateway.Tests/Gateway/Dashboard/DashboardBrowseAndAlarmModelTests.cs b/src/MxGateway.Tests/Gateway/Dashboard/DashboardBrowseAndAlarmModelTests.cs index e226f10..88aa7ff 100644 --- a/src/MxGateway.Tests/Gateway/Dashboard/DashboardBrowseAndAlarmModelTests.cs +++ b/src/MxGateway.Tests/Gateway/Dashboard/DashboardBrowseAndAlarmModelTests.cs @@ -126,4 +126,17 @@ public sealed class DashboardBrowseAndAlarmModelTests Assert.True(unackedRow.IsUnacknowledged); Assert.False(ackedRow.IsUnacknowledged); } + + [Fact] + public void FormatValue_AndDataType_RenderArrayElementsAndElementType() + { + MxArray array = new() { ElementDataType = MxDataType.Double }; + array.Dimensions.Add(3u); + array.DoubleValues = new DoubleArray(); + array.DoubleValues.Values.Add(new[] { 1.5, 2.25, 3.0 }); + MxValue value = new() { ArrayValue = array }; + + Assert.Equal("[1.5, 2.25, 3]", DashboardMxValueFormatter.FormatValue(value)); + Assert.Equal("Double[3]", DashboardMxValueFormatter.FormatDataType(value)); + } }