tbf/TBF/Rig/Output/DB/ResultsWriter/ResultsWriter.cs
Marek Frniak d3c8813012 <Fix>: Correct test-dependent result evaluation in ResultsWriter, increase revision to 3.9.3145.101
Cause:

- ResultsWriter evaluated selected test-dependent items without the actual TestID.
- This caused values such as Test passed(), timestamps, flow, pressure, temperature, conductivity and error data to be empty or incorrect.

Solution:

1. Fixed test-aware result evaluation
   - Uses published regular meter test results.
   - Passes mtr.Name() as TestID to item.Print(wm, testId).

2. Restored correct test result mapping
   - Test-dependent values are now resolved from the correct TestRslt / MeterTestRslt.

3. Increased revision
   - Updated revision to 3.9.3145.101.
2026-09-03 10:00:40 +02:00

1278 lines
39 KiB
C#

///
/// Copyright (c) 2026 Sensus Slovensko a.s.
///
using Common;
using log4net;
using Results.Entities;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using TBF.Rig.Generic;
using TBF.Rig.Output.DataStorage.UniDataStorageWriter;
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters;
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Interfaces;
using TBF.Rig.Sequences;
namespace TBF.Rig.Output.DB.ResultsWriter
{
/// <summary>
/// Writes evaluated water-meter results through UniDataStorageWriter.
/// </summary>
/// <remarks>
/// Normal Insert mode preserves the original Caption/value behavior.
/// XML payload targets generate a clean XML document derived from the
/// configured customer reference XML. XML element and attribute names have
/// no predefined semantics in ResultsWriter. Every selected TBF result is
/// written only to the destination explicitly selected by the user.
/// The generated payload can either be sent to a Microsoft SQL stored
/// procedure or written directly to an XML file through
/// UniDataStorageWriter.
/// </remarks>
public class ResultsWriter :
ComponentBase,
IOperation,
GenericDevices.IResultsWriter,
Generic.IDevice
{
private const string RepeatPrefix = "repeat:";
private static readonly ILog log =
LogManager.GetLogger(
typeof(ResultsWriter));
private ResultsWriterCfg resultsWriterCfg;
private Batch batch;
private bool opCompleted;
private bool anyError;
private enum OpState
{
None,
WriteResultsScheduled,
WriteResultsRunning,
}
private OpState currentOpState;
private TBF.Rig.Output.DataStorage.UniDataStorageWriter.Writer
dataStorageWriter;
public ResultsWriter()
{
}
public ResultsWriter(
IComponentCfg cfg,
IList<IComponent> components)
: base(cfg)
{
resultsWriterCfg =
cfg as ResultsWriterCfg;
if (resultsWriterCfg == null)
throw new ArgumentException(
"resultsWriterCfg");
currentOpState =
OpState.None;
log.Warn(
ToString());
//
// Do not require the parent to be already instantiated here.
// TBF component loading is sequential and the runtime component list
// passed to this constructor may not yet contain every configured
// component. Resolve immediately when possible and defer otherwise.
//
TryAssignParent(
components);
if (dataStorageWriter == null)
{
log.WarnFormat(
"Parent '{0}' is not available yet. Parent resolution will be deferred until ResultsWriter is used.",
resultsWriterCfg.ParentName);
}
}
public ResultsWriter(
IComponentCfg cfg)
: base(cfg)
{
resultsWriterCfg =
cfg as ResultsWriterCfg;
if (resultsWriterCfg == null)
throw new ArgumentException(
"resultsWriterCfg");
currentOpState =
OpState.None;
log.Warn(
ToString());
}
public override string ToString()
{
return string.Format(
"{0}({1})",
ClassName,
Cfg.ToString(-1));
}
public override void Initialize()
{
}
public void RunDeviceBefore()
{
}
public void RunDeviceAfter()
{
}
public void StopDevice()
{
}
public void StopDevice2()
{
}
public IOperation ProcessResultsOp(
Batch batch)
{
if (!resultsWriterCfg.Enabled)
return null;
if (currentOpState ==
OpState.WriteResultsRunning)
{
throw new Exception(
"Sequence error");
}
this.batch =
batch;
currentOpState =
OpState.WriteResultsScheduled;
return this;
}
public void Start()
{
if (currentOpState ==
OpState.WriteResultsScheduled)
{
currentOpState =
OpState.WriteResultsRunning;
}
opCompleted = false;
anyError = false;
}
public Event Run()
{
log.WarnFormat(
"{0} : Run() : currentOp = {1}",
Name,
currentOpState);
if (currentOpState !=
OpState.WriteResultsRunning)
{
return Event.None;
}
if (resultsWriterCfg.DebugLevel ==
DebugMode.Simulate)
{
return Event.ResultsWritten;
}
if (batch == null ||
batch.WaterMeters == null ||
batch.WaterMeters.Count == 0)
{
return Event.ResultsWritten;
}
if (opCompleted)
{
return anyError
? Event.ErrorProcessingResults
: Event.ResultsWritten;
}
opCompleted = true;
try
{
WriteBatchResults(
batch);
}
catch (Exception exc)
{
anyError = true;
log.ErrorFormat(
"Failed to write results by ResultsWriter: {0}",
exc);
}
return anyError
? Event.ErrorProcessingResults
: Event.ResultsWritten;
}
public void Stop()
{
currentOpState =
OpState.None;
}
/// <summary>
/// Writes all enabled water meters in a batch.
/// </summary>
/// <remarks>
/// XML payload targets are generated once per batch. Every enabled water
/// meter contributes one logical repeat record to the same XML document.
///
/// Legacy Insert output keeps its existing behavior: every published
/// regular meter test is written with its concrete test ID context.
/// </remarks>
public void WriteBatchResults(
Batch batch)
{
if (batch == null)
throw new ArgumentNullException(
nameof(batch));
EnsureParentInitialized();
resultsWriterCfg.UpdateRuntimeModel();
WriterCfg storageCfg =
GetDataStorageWriterConfiguration();
if (storageCfg.UsesXmlPayload())
{
WriteXmlBatchResults(
batch,
storageCfg);
return;
}
if (storageCfg.WriteMode !=
WriteMode.Insert)
{
throw new NotSupportedException(
string.Format(
"ResultsWriter does not support UniDataStorageWriter target '{0}' / '{1}' / '{2}'.",
storageCfg.DataStorageType,
storageCfg.TechnologyType,
storageCfg.WriteMode));
}
foreach (WaterMeter wm
in batch.WaterMeters)
{
if (wm == null ||
wm.Disabled)
{
continue;
}
//
// Insert mode needs the same test context as the legacy TBF
// result writers. One row is generated for every published
// regular meter test and all selected items are evaluated
// with the actual mtr.Name() as test ID.
//
WriteInsertResults(
wm,
storageCfg);
}
}
/// <summary>
/// Generates and writes one XML payload for the complete batch.
/// </summary>
private void WriteXmlBatchResults(
Batch batch,
WriterCfg storageCfg)
{
IList<WaterMeter> enabledMeters =
batch.WaterMeters
.Where(
wm =>
wm != null &&
!wm.Disabled)
.ToList();
if (enabledMeters.Count == 0)
return;
XmlPayloadRequestBuilder payloadBuilder =
new XmlPayloadRequestBuilder(
storageCfg);
PayloadGenerationRequest generationRequest =
BuildBatchPayloadGenerationRequest(
enabledMeters);
string payloadIdentifier =
BuildBatchPayloadIdentifier(
enabledMeters);
XmlPayloadBuildResult buildResult;
if (storageCfg.IsStoredProcedurePayloadTarget())
{
buildResult =
payloadBuilder.Build(
generationRequest,
payloadIdentifier);
}
else if (storageCfg.IsXmlFileTarget())
{
buildResult =
payloadBuilder.BuildFile(
generationRequest,
payloadIdentifier);
}
else
{
throw new NotSupportedException(
string.Format(
"Unsupported XML payload target '{0}' / '{1}' / '{2}'.",
storageCfg.DataStorageType,
storageCfg.TechnologyType,
storageCfg.WriteMode));
}
if (!string.IsNullOrWhiteSpace(
buildResult.ArchiveFilePath))
{
log.InfoFormat(
"XML payload archived to '{0}'.",
buildResult.ArchiveFilePath);
}
if (!string.IsNullOrWhiteSpace(
buildResult.OutputFileName))
{
log.InfoFormat(
"XML batch payload generated: {0}",
buildResult.OutputFileName);
}
WriterDiagnosticResult result =
dataStorageWriter.SetData(
buildResult.Request);
if (!result.Success)
{
throw new Exception(
result.Message);
}
log.WarnFormat(
"ResultsWriter wrote one XML payload for {0} enabled water meter(s). Target={1}, Technology={2}, Mode={3}.",
enabledMeters.Count,
storageCfg.DataStorageType,
storageCfg.TechnologyType,
storageCfg.WriteMode);
}
/// <summary>
/// Creates a stable identifier used by the XML request builder for one batch.
/// </summary>
private string BuildBatchPayloadIdentifier(
IList<WaterMeter> enabledMeters)
{
if (enabledMeters == null ||
enabledMeters.Count == 0)
{
return "BATCH";
}
string firstSerial =
Convert.ToString(
enabledMeters[0].SerialNr);
if (string.IsNullOrWhiteSpace(
firstSerial))
{
return "BATCH";
}
return firstSerial;
}
/// <summary>
/// Generates a dry-run XML payload without calling the database.
/// </summary>
/// <param name="simulationBatch">
/// Batch used to evaluate one-time TBF mappings and provide preview context.
/// </param>
/// <param name="simulatedTestCount">
/// Number of logical repeated result records generated for the preview.
/// </param>
public XmlPayloadBuildResult GeneratePreviewPayload(
Batch simulationBatch,
int simulatedTestCount)
{
if (simulationBatch == null)
throw new ArgumentNullException(
nameof(simulationBatch));
if (simulatedTestCount < 0)
throw new ArgumentOutOfRangeException(
nameof(simulatedTestCount));
EnsureParentInitialized();
resultsWriterCfg.UpdateRuntimeModel();
WriterCfg storageCfg =
GetDataStorageWriterConfiguration();
if (!storageCfg.UsesXmlPayload())
{
throw new InvalidOperationException(
"XML preview is available only for an XML payload target.");
}
WaterMeter wm =
simulationBatch.WaterMeters
.FirstOrDefault(
meter =>
meter != null &&
!meter.Disabled);
if (wm == null)
{
throw new InvalidOperationException(
"Simulation batch does not contain an enabled water meter.");
}
XmlPayloadRequestBuilder builder =
new XmlPayloadRequestBuilder(
storageCfg);
PayloadGenerationRequest request =
BuildPreviewGenerationRequest(
wm,
simulatedTestCount);
request.RepeatPrototypePath =
builder.GetDefaultRepeatPrototypePath();
return builder.BuildPreview(
request,
Convert.ToString(
wm.SerialNr));
}
/// <summary>
/// Writes Insert rows for one water meter using the actual TBF test context.
/// </summary>
/// <remarks>
/// One row is written for every published regular meter test.
/// The actual test ID is passed to WMeterRsltItemSpec.Print().
/// </remarks>
private void WriteInsertResults(
WaterMeter wm,
WriterCfg storageCfg)
{
IList<MeterTestRslt> publishedTests =
wm.RegularMeterTestRslts()
.Where(
mtr =>
mtr != null &&
mtr.Publish() ==
Publish.Always)
.ToList();
//
// Preserve meter/batch data even if there is no published test.
//
if (publishedTests.Count == 0)
{
DataWriteRequest fallbackRequest =
BuildInsertRequest(
wm,
string.Empty);
WriteInsertRequest(
wm,
string.Empty,
fallbackRequest,
storageCfg);
return;
}
foreach (MeterTestRslt mtr
in publishedTests)
{
string testId =
mtr.Name() ??
string.Empty;
DataWriteRequest request =
BuildInsertRequest(
wm,
testId);
WriteInsertRequest(
wm,
testId,
request,
storageCfg);
}
}
/// <summary>
/// Builds one Insert request for one water meter and one concrete test.
/// </summary>
private DataWriteRequest BuildInsertRequest(
WaterMeter wm,
string testId)
{
DataWriteRequest request =
new DataWriteRequest
{
Mode = WriteMode.Insert
};
if (resultsWriterCfg.SelectedItems == null)
return request;
foreach (Results.WMeterRsltItemSpec item
in resultsWriterCfg.SelectedItems)
{
if (item == null ||
string.IsNullOrWhiteSpace(
item.Caption))
{
continue;
}
request.InsertItems.Add(
new InsertWriteItem
{
ColumnName =
item.Caption,
Value =
item.Print(
wm,
testId)
});
}
return request;
}
/// <summary>
/// Executes one prepared Insert request.
/// </summary>
private void WriteInsertRequest(
WaterMeter wm,
string testId,
DataWriteRequest request,
WriterCfg storageCfg)
{
if (request == null ||
request.InsertItems.Count == 0)
{
log.WarnFormat(
"No values to write for WM position {0}",
wm.WMPosition);
return;
}
WriterDiagnosticResult result =
dataStorageWriter.SetData(
request);
if (!result.Success)
{
throw new Exception(
result.Message);
}
log.WarnFormat(
"ResultsWriter wrote WM position {0}, TestID='{1}'. Target={2}, Technology={3}, Mode={4}.",
wm.WMPosition,
testId,
storageCfg.DataStorageType,
storageCfg.TechnologyType,
storageCfg.WriteMode);
}
/// <summary>
/// Creates one runtime XML mapping request for the complete batch.
/// </summary>
/// <param name="enabledMeters">
/// Enabled water meters that must be represented in the generated payload.
/// </param>
/// <remarks>
/// <para>
/// Every enabled water meter creates one logical repeat record. All
/// destinations located inside the repeating XML prototype are populated
/// into that same record.
/// </para>
///
/// <para>
/// Result evaluation first uses the meter-level context. If that does not
/// produce a value, the same result item is evaluated against the actual
/// published measurement-test IDs in procedure order. This preserves
/// meter-level values while also allowing test-result variables to be
/// populated in the XML record.
/// </para>
/// </remarks>
private PayloadGenerationRequest BuildBatchPayloadGenerationRequest(
IList<WaterMeter> enabledMeters)
{
PayloadGenerationRequest request =
new PayloadGenerationRequest();
if (enabledMeters == null ||
enabledMeters.Count == 0 ||
resultsWriterCfg.SelectedItems == null)
{
return request;
}
List<Results.WMeterRsltItemSpec> singleItems =
resultsWriterCfg.SelectedItems
.Where(
item =>
item != null &&
!string.IsNullOrWhiteSpace(
item.Caption) &&
!IsRepeatDestination(
item.Caption))
.ToList();
List<Results.WMeterRsltItemSpec> repeatItems =
resultsWriterCfg.SelectedItems
.Where(
item =>
item != null &&
!string.IsNullOrWhiteSpace(
item.Caption) &&
IsRepeatDestination(
item.Caption))
.ToList();
//
// One-time XML destinations exist only once in the document.
// Evaluate them from the first enabled water meter. This preserves
// the existing one-time mapping model while repeat destinations are
// created once for every enabled water meter.
//
WaterMeter firstMeter =
enabledMeters[0];
for (int singleIndex = 0;
singleIndex < singleItems.Count;
singleIndex++)
{
Results.WMeterRsltItemSpec item =
singleItems[
singleIndex];
string sourceKey =
string.Format(
CultureInfo.InvariantCulture,
"Batch.Single.{0}",
singleIndex);
request.SingleMappings.Add(
new PayloadMapping
{
SourceKey =
sourceKey,
DestinationPath =
item.Caption
});
request.SingleValues.SetValue(
sourceKey,
EvaluateXmlMappedValue(
firstMeter,
item));
}
//
// Every enabled water meter becomes one logical repeat record.
//
for (int meterIndex = 0;
meterIndex < enabledMeters.Count;
meterIndex++)
{
WaterMeter wm =
enabledMeters[
meterIndex];
PayloadRepeatRecord record =
new PayloadRepeatRecord();
for (int repeatIndex = 0;
repeatIndex < repeatItems.Count;
repeatIndex++)
{
Results.WMeterRsltItemSpec item =
repeatItems[
repeatIndex];
string sourceKey =
string.Format(
CultureInfo.InvariantCulture,
"Batch.Repeat.{0}.{1}",
meterIndex,
repeatIndex);
AddRecordMapping(
record,
sourceKey,
item.Caption,
EvaluateXmlMappedValue(
wm,
item));
}
if (record.Mappings.Count > 0)
{
request.RepeatRecords.Add(
record);
}
}
return request;
}
/// <summary>
/// Evaluates one configured result item for XML output.
/// </summary>
/// <remarks>
/// Meter/batch variables normally return a value directly from
/// Print(wm). Test-result variables require a concrete test ID. For those
/// variables the method evaluates the published regular meter tests in
/// measurement-procedure order and uses the first non-empty value.
///
/// This fallback is intentionally applied only when the meter-level
/// evaluation is empty.
/// </remarks>
private string EvaluateXmlMappedValue(
WaterMeter wm,
Results.WMeterRsltItemSpec item)
{
if (wm == null ||
item == null)
{
return string.Empty;
}
string meterValue =
item.Print(
wm);
if (!string.IsNullOrWhiteSpace(
meterValue))
{
return meterValue;
}
IList<MeterTestRslt> publishedTests =
wm.RegularMeterTestRslts()
.Where(
mtr =>
mtr != null &&
mtr.Publish() ==
Publish.Always)
.ToList();
string firstResolvedValue =
string.Empty;
string firstResolvedTestId =
string.Empty;
foreach (MeterTestRslt mtr
in publishedTests)
{
string testId =
mtr.Name() ??
string.Empty;
string testValue =
item.Print(
wm,
testId);
if (string.IsNullOrWhiteSpace(
testValue))
{
continue;
}
if (string.IsNullOrWhiteSpace(
firstResolvedValue))
{
firstResolvedValue =
testValue;
firstResolvedTestId =
testId;
continue;
}
//
// A single XML destination can hold only one scalar value.
// If more than one test produces a different value for the same
// mapped result, preserve deterministic procedure order and keep
// the first value, but make the ambiguity visible in the log.
//
if (!string.Equals(
firstResolvedValue,
testValue,
StringComparison.Ordinal))
{
log.WarnFormat(
"XML mapping '{0}' produced values in more than one test context for WM position {1}. Using first test '{2}'.",
item.Caption,
wm.WMPosition,
firstResolvedTestId);
break;
}
}
return firstResolvedValue;
}
/// <summary>
/// Creates a preview request with an exact number of simulated repeat records.
/// </summary>
private PayloadGenerationRequest BuildPreviewGenerationRequest(
WaterMeter wm,
int simulatedTestCount)
{
PayloadGenerationRequest request =
new PayloadGenerationRequest();
List<Results.WMeterRsltItemSpec> repeatItems =
new List<Results.WMeterRsltItemSpec>();
int sourceIndex = 0;
if (resultsWriterCfg.SelectedItems != null)
{
foreach (Results.WMeterRsltItemSpec item
in resultsWriterCfg.SelectedItems)
{
if (item == null ||
string.IsNullOrWhiteSpace(
item.Caption))
{
continue;
}
if (IsRepeatDestination(
item.Caption))
{
//
// A repeating mapping defines a destination inside the
// repeat prototype. The mapping itself must not create a
// separate repeat record.
//
repeatItems.Add(
item);
continue;
}
string sourceKey =
"Preview.Single." +
sourceIndex.ToString(
CultureInfo.InvariantCulture);
string previewValue =
item.Print(
wm);
//
// A minimal simulation batch does not contain every possible
// TBF result source. Use a clearly synthetic type-appropriate
// value when the evaluated preview value is unavailable.
//
if (string.IsNullOrWhiteSpace(
previewValue))
{
previewValue =
CreateUniversalSimulationValue(
item,
sourceIndex);
}
sourceIndex++;
request.SingleMappings.Add(
new PayloadMapping
{
SourceKey =
sourceKey,
DestinationPath =
item.Caption
});
request.SingleValues.SetValue(
sourceKey,
previewValue);
}
}
//
// Create the requested number of logical repeat records. Every record
// receives all configured repeating mappings, so one simulated record
// is structurally equivalent to one generated runtime record.
//
for (int recordIndex = 0;
recordIndex < simulatedTestCount &&
repeatItems.Count > 0;
recordIndex++)
{
PayloadRepeatRecord record =
new PayloadRepeatRecord();
for (int repeatIndex = 0;
repeatIndex < repeatItems.Count;
repeatIndex++)
{
Results.WMeterRsltItemSpec item =
repeatItems[
repeatIndex];
string sourceKey =
string.Format(
CultureInfo.InvariantCulture,
"Preview.Repeat.{0}.{1}",
recordIndex,
repeatIndex);
int simulationValueIndex =
recordIndex *
repeatItems.Count +
repeatIndex;
AddRecordMapping(
record,
sourceKey,
item.Caption,
CreateUniversalSimulationValue(
item,
simulationValueIndex));
}
request.RepeatRecords.Add(
record);
}
return request;
}
private void AddRecordMapping(
PayloadRepeatRecord record,
string sourceKey,
string destinationPath,
object value)
{
if (string.IsNullOrWhiteSpace(
destinationPath))
{
return;
}
record.Mappings.Add(
new PayloadMapping
{
SourceKey =
sourceKey,
DestinationPath =
destinationPath
});
record.Values.SetValue(
sourceKey,
value);
}
/// <summary>
/// Creates a clearly synthetic preview value according to the logical
/// type of a configured TBF result item.
/// </summary>
/// <param name="item">
/// Result item whose logical type should be simulated.
/// </param>
/// <param name="simulationIndex">
/// Zero-based simulation value index.
/// </param>
/// <returns>
/// A non-empty, type-appropriate value suitable for XML preview output.
/// </returns>
/// <remarks>
/// <para>
/// Preview values are intentionally deterministic and visibly synthetic.
/// They are used only when a preview source has no real simulation value,
/// or when repeated test records are created without real measured data.
/// </para>
///
/// <para>
/// The method uses the result category description instead of depending
/// on concrete enum member names. Physical quantity categories therefore
/// naturally fall back to a numeric simulation value.
/// </para>
/// </remarks>
private string CreateUniversalSimulationValue(
Results.WMeterRsltItemSpec item,
int simulationIndex)
{
if (item == null)
{
return string.Format(
CultureInfo.InvariantCulture,
"SIMULATION_{0}",
simulationIndex + 1);
}
string category =
Common.GetDescription.ToDescription(
item.Category);
string normalizedCategory =
(category ?? string.Empty)
.Trim()
.ToUpperInvariant();
//
// Boolean
//
if (normalizedCategory.Contains(
"BOOLEAN"))
{
return simulationIndex % 2 == 0
? "True"
: "False";
}
//
// Date and time
//
if (normalizedCategory.Contains(
"DATE") ||
normalizedCategory.Contains(
"TIME"))
{
return new DateTime(
2099,
1,
1,
12,
0,
0)
.AddSeconds(
simulationIndex + 1)
.ToString(
"yyyy-MM-dd HH:mm:ss.fff",
CultureInfo.InvariantCulture);
}
//
// Explicit textual categories
//
if (normalizedCategory.Contains(
"STRING"))
{
return string.Format(
CultureInfo.InvariantCulture,
"SIMULATION_{0}",
simulationIndex + 1);
}
if (normalizedCategory.Contains(
"ENUMERATED"))
{
return string.Format(
CultureInfo.InvariantCulture,
"SIMULATION_ENUM_{0}",
simulationIndex + 1);
}
if (normalizedCategory.Contains(
"ERROR"))
{
return string.Format(
CultureInfo.InvariantCulture,
"SIMULATION_ERROR_{0}",
simulationIndex + 1);
}
//
// Flow, humidity, length, mass, number, pressure, pulses,
// temperature, volume and other physical quantities are represented
// by a valid invariant numeric value.
//
return (900000 +
simulationIndex + 1)
.ToString(
CultureInfo.InvariantCulture);
}
private bool IsRepeatDestination(
string destinationPath)
{
return !string.IsNullOrWhiteSpace(
destinationPath) &&
destinationPath.StartsWith(
RepeatPrefix,
StringComparison.Ordinal);
}
private WriterCfg GetDataStorageWriterConfiguration()
{
WriterCfg storageCfg =
dataStorageWriter.Cfg
as WriterCfg;
if (storageCfg == null)
{
throw new InvalidOperationException(
"Parent UniDataStorageWriter configuration is not a WriterCfg.");
}
return storageCfg;
}
/// <summary>
/// Resolves the configured UniDataStorageWriter parent from the complete
/// runtime component collection.
/// </summary>
public void InitializeParent()
{
EnsureParentInitialized();
}
/// <summary>
/// Tries to resolve the configured parent from a supplied component list.
/// </summary>
/// <remarks>
/// Parent-name comparison is intentionally trimmed and case-insensitive
/// because component names originate from persisted user configuration.
/// </remarks>
private void TryAssignParent(
IEnumerable<IComponent> components)
{
if (components == null ||
string.IsNullOrWhiteSpace(
resultsWriterCfg.ParentName))
{
return;
}
string configuredParentName =
resultsWriterCfg.ParentName.Trim();
IComponent parent =
components.FirstOrDefault(
component =>
component != null &&
string.Equals(
(component.Name ??
string.Empty).Trim(),
configuredParentName,
StringComparison.OrdinalIgnoreCase));
if (parent == null)
return;
TBF.Rig.Output.DataStorage
.UniDataStorageWriter.Writer writer =
parent as
TBF.Rig.Output.DataStorage
.UniDataStorageWriter.Writer;
if (writer == null)
{
throw new Exception(
string.Format(
"Parent '{0}' was found, but its runtime type '{1}' is not UniDataStorageWriter.Writer.",
resultsWriterCfg.ParentName,
parent.GetType().FullName));
}
dataStorageWriter =
writer;
}
/// <summary>
/// Resolves the parent after all TBF components have had a chance to load.
/// </summary>
private void EnsureParentInitialized()
{
if (dataStorageWriter != null)
return;
if (string.IsNullOrWhiteSpace(
resultsWriterCfg.ParentName))
{
throw new InvalidOperationException(
"UniDataStorageWriter parent is not configured.");
}
IComponent parent =
TbfComponents.FindComponent(
resultsWriterCfg.ParentName.Trim());
if (parent == null)
{
throw new Exception(
string.Format(
"Parent '{0}' was not found after component loading completed.",
resultsWriterCfg.ParentName));
}
dataStorageWriter =
parent as
TBF.Rig.Output.DataStorage
.UniDataStorageWriter.Writer;
if (dataStorageWriter == null)
{
throw new Exception(
string.Format(
"Parent '{0}' of runtime type '{1}' is not UniDataStorageWriter.Writer.",
resultsWriterCfg.ParentName,
parent.GetType().FullName));
}
}
}
}