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) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-21 14:34:21 -04:00
parent cec84bf572
commit 0a54fa5e35
2 changed files with 73 additions and 3 deletions
@@ -10,6 +10,9 @@ namespace MxGateway.Server.Dashboard;
/// </summary>
public static class DashboardMxValueFormatter
{
/// <summary>Maximum array elements rendered inline before the value is truncated.</summary>
private const int MaxArrayElements = 24;
/// <summary>Formats the value payload of an <see cref="MxValue"/>.</summary>
/// <param name="value">The value to format; may be null.</param>
/// <returns>A display string — never null.</returns>
@@ -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
/// <summary>Formats the MXAccess data type of an <see cref="MxValue"/>.</summary>
/// <param name="value">The value whose data type to describe; may be null.</param>
/// <returns>The data-type name — never null.</returns>
/// <returns>The data-type name — never null. Arrays render as <c>Element[dims]</c>.</returns>
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<string> 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}]";
}
}
@@ -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));
}
}