fix(dcl): format ArrayValue objects as comma-separated strings for display
ArrayValue from LmxProxy client was showing as type name in debug views. Added ValueFormatter utility and NormalizeValue in LmxProxyDataConnection to convert arrays at the adapter boundary. DateTime arrays remain as "System.DateTime[]" due to server-side v1 string serialization.
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
@using ScadaLink.Commons.Interfaces.Repositories
|
||||
@using ScadaLink.Commons.Messages.DebugView
|
||||
@using ScadaLink.Commons.Messages.Streaming
|
||||
@using ScadaLink.Commons.Types
|
||||
@using ScadaLink.Commons.Types.Enums
|
||||
@using ScadaLink.Communication
|
||||
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDeployment)]
|
||||
@@ -91,7 +92,7 @@
|
||||
{
|
||||
<tr>
|
||||
<td class="small">@av.AttributeName</td>
|
||||
<td class="small font-monospace"><strong>@av.Value</strong></td>
|
||||
<td class="small font-monospace"><strong>@ValueFormatter.FormatDisplayValue(av.Value)</strong></td>
|
||||
<td>
|
||||
<span class="badge @(av.Quality == "Good" ? "bg-success" : "bg-warning text-dark")">@av.Quality</span>
|
||||
</td>
|
||||
|
||||
59
src/ScadaLink.Commons/Types/ValueFormatter.cs
Normal file
59
src/ScadaLink.Commons/Types/ValueFormatter.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System.Collections;
|
||||
using System.Reflection;
|
||||
|
||||
namespace ScadaLink.Commons.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Formats attribute values for display. Handles scalar types directly
|
||||
/// and uses reflection to extract array contents from complex types
|
||||
/// (e.g., LmxProxy ArrayValue) rather than showing the type name.
|
||||
/// </summary>
|
||||
public static class ValueFormatter
|
||||
{
|
||||
/// <summary>
|
||||
/// Formats a value for display as a string. Returns the value's natural
|
||||
/// string representation for scalars, and comma-separated elements for
|
||||
/// array/collection types.
|
||||
/// </summary>
|
||||
public static string FormatDisplayValue(object? value)
|
||||
{
|
||||
if (value is null) return "";
|
||||
if (value is string s) return s;
|
||||
if (value is IFormattable) return value.ToString() ?? "";
|
||||
|
||||
// Check if it's an array-like container with typed sub-collections
|
||||
// (e.g., LmxProxy ArrayValue with BoolValues, Int32Values, etc.)
|
||||
var type = value.GetType();
|
||||
if (type.Namespace?.Contains("LmxProxy") == true || type.Name == "ArrayValue")
|
||||
{
|
||||
return FormatArrayContainer(value, type);
|
||||
}
|
||||
|
||||
// Fallback for IEnumerable (generic collections, arrays)
|
||||
if (value is IEnumerable enumerable)
|
||||
{
|
||||
return string.Join(",", enumerable.Cast<object?>().Select(e => e?.ToString() ?? ""));
|
||||
}
|
||||
|
||||
return value.ToString() ?? "";
|
||||
}
|
||||
|
||||
private static string FormatArrayContainer(object container, Type type)
|
||||
{
|
||||
// Look for the first non-null property that has a Values list
|
||||
foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
var propValue = prop.GetValue(container);
|
||||
if (propValue is null) continue;
|
||||
|
||||
// Check if this property has a Values sub-property (e.g., BoolArray.Values)
|
||||
var valuesProp = propValue.GetType().GetProperty("Values");
|
||||
if (valuesProp?.GetValue(propValue) is IEnumerable values)
|
||||
{
|
||||
return string.Join(",", values.Cast<object?>().Select(e => e?.ToString() ?? ""));
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using Akka.Actor;
|
||||
using Akka.Event;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using ScadaLink.Commons.Messages.Streaming;
|
||||
using ScadaLink.Commons.Types;
|
||||
using ScadaLink.Communication.Grpc;
|
||||
using AlarmState = ScadaLink.Commons.Types.Enums.AlarmState;
|
||||
|
||||
@@ -38,7 +39,7 @@ public class StreamRelayActor : ReceiveActor
|
||||
InstanceUniqueName = msg.InstanceUniqueName,
|
||||
AttributePath = msg.AttributePath,
|
||||
AttributeName = msg.AttributeName,
|
||||
Value = msg.Value?.ToString() ?? "",
|
||||
Value = ValueFormatter.FormatDisplayValue(msg.Value),
|
||||
Quality = MapQuality(msg.Quality),
|
||||
Timestamp = Timestamp.FromDateTimeOffset(msg.Timestamp)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging;
|
||||
using ScadaLink.Commons.Interfaces.Protocol;
|
||||
using ScadaLink.Commons.Types.Enums;
|
||||
using ZB.MOM.WW.LmxProxy.Client.Domain;
|
||||
using ScadaLink.Commons.Types;
|
||||
using QualityCode = ScadaLink.Commons.Interfaces.Protocol.QualityCode;
|
||||
using WriteResult = ScadaLink.Commons.Interfaces.Protocol.WriteResult;
|
||||
|
||||
@@ -76,7 +77,7 @@ public class LmxProxyDataConnection : IDataConnection
|
||||
{
|
||||
var vtq = await _client!.ReadAsync(tagPath, cancellationToken);
|
||||
var quality = MapQuality(vtq.Quality);
|
||||
var tagValue = new TagValue(vtq.Value, quality, new DateTimeOffset(vtq.Timestamp, TimeSpan.Zero));
|
||||
var tagValue = new TagValue(NormalizeValue(vtq.Value), quality, new DateTimeOffset(vtq.Timestamp, TimeSpan.Zero));
|
||||
|
||||
return vtq.Quality.IsBad()
|
||||
? new ReadResult(false, tagValue, "LmxProxy read returned bad quality")
|
||||
@@ -100,7 +101,7 @@ public class LmxProxyDataConnection : IDataConnection
|
||||
foreach (var (tag, vtq) in vtqs)
|
||||
{
|
||||
var quality = MapQuality(vtq.Quality);
|
||||
var tagValue = new TagValue(vtq.Value, quality, new DateTimeOffset(vtq.Timestamp, TimeSpan.Zero));
|
||||
var tagValue = new TagValue(NormalizeValue(vtq.Value), quality, new DateTimeOffset(vtq.Timestamp, TimeSpan.Zero));
|
||||
results[tag] = vtq.Quality.IsBad()
|
||||
? new ReadResult(false, tagValue, "LmxProxy read returned bad quality")
|
||||
: new ReadResult(true, tagValue, null);
|
||||
@@ -177,7 +178,7 @@ public class LmxProxyDataConnection : IDataConnection
|
||||
(path, vtq) =>
|
||||
{
|
||||
var quality = MapQuality(vtq.Quality);
|
||||
callback(path, new TagValue(vtq.Value, quality, new DateTimeOffset(vtq.Timestamp, TimeSpan.Zero)));
|
||||
callback(path, new TagValue(NormalizeValue(vtq.Value), quality, new DateTimeOffset(vtq.Timestamp, TimeSpan.Zero)));
|
||||
},
|
||||
onStreamError: ex =>
|
||||
{
|
||||
@@ -231,6 +232,18 @@ public class LmxProxyDataConnection : IDataConnection
|
||||
Disconnected?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a Vtq value for consumption by the rest of the system.
|
||||
/// Converts LmxProxy ArrayValue objects to comma-separated strings
|
||||
/// so downstream code doesn't need to know about LmxProxy domain types.
|
||||
/// </summary>
|
||||
private static object? NormalizeValue(object? value) => value switch
|
||||
{
|
||||
null or string => value,
|
||||
IFormattable => value,
|
||||
_ => ValueFormatter.FormatDisplayValue(value)
|
||||
};
|
||||
|
||||
private static QualityCode MapQuality(Quality quality)
|
||||
{
|
||||
if (quality.IsGood()) return QualityCode.Good;
|
||||
|
||||
Reference in New Issue
Block a user