tbf/TBF/Rig/Output/DB/ResultsWriter/XmlDestinationPickerDlg.cs
Marek Frniak 054075a341 <Feat>: Add XML result generation with file and MSSQL output support, increase revision to 3.9.3145.100
Cause:

- ResultsWriter required a generic way to generate customer-specific XML result data from TBF measurement results.
- The customer reference XML contains example runtime values and repeated result structures, so it cannot be used directly as the generated output.
- TBF result variables must be explicitly mapped to destinations in the customer XML structure.
- The generated XML result data must support two output targets:
  - direct creation of an XML file,
  - delivery of the XML payload to a Microsoft SQL stored procedure.
- Preview generation must allow the XML structure and configured mappings to be verified without executing the production database write.
- Increase revision to 3.9.3145.100.

Solution:

1. Added XML reference analysis
   - Creates a clean base XML structure.
   - Extracts the repeating result prototype.
   - Prevents sample runtime values from the customer reference XML from leaking into generated results.
2. Added configurable TBF-to-XML result mapping
   - Allows explicit mapping of TBF result variables to customer XML destinations.
   - Supports one-time and repeating XML destinations.
   - Keeps the mapping independent of the semantic meaning of customer XML attribute names.
3. Added XML destination viewer and configurator
   - Shows the current mapping.
   - Highlights repeating destinations.
   - Identifies already used one-time destinations.
4. Added runtime XML result generation
   - Uses the configured TBF-to-XML mappings.
   - Uses measurement procedure result data.
   - Builds the output from the clean base XML and repeating XML prototype.
   - Generates repeated result records according to the executed measurement procedure.
5. Added simulation-based Preview request
   - Generates a complete XML payload using simulation values.
   - Allows XML structure and mapping verification before production execution.
   - Does not execute the production stored procedure.
6. Added direct XML file output support to UniDataStorageWriter
   - Supports File / .xml as a physical output target.
   - Creates the generated XML result file in the configured output directory.
7. Added Microsoft SQL stored-procedure XML output
   - Supports Microsoft SQL / StoredProcedure as a physical output target.
   - Passes the generated XML payload through the configured stored procedure parameter.
8. Added optional XML payload archiving
   - Allows generated XML payloads to be stored in the configured Payload archive.
   - Can be used together with the Microsoft SQL stored-procedure output.
9. Kept ResultsWriter independent of the physical output target
   - ResultsWriter generates the result payload.
   - UniDataStorageWriter decides how and where the payload is physically written.
   - The same ResultsWriter XML generation mechanism is therefore used for both XML file and MSSQL outputs.
10. Increased revision
   - Updated revision to 3.9.3145.100.
2026-09-02 12:05:10 +02:00

587 lines
16 KiB
C#

///
/// Copyright (c) 2026 Sensus Slovensko a.s.
///
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters;
using static System.Windows.Forms.VisualStyles.VisualStyleElement.Window;
namespace TBF.Rig.Output.DB.ResultsWriter
{
/// <summary>
/// Displays the structural model of an XML payload example and allows
/// selection of one logical XML destination.
/// </summary>
/// <remarks>
/// <para>
/// Sample XML values are intentionally not displayed. The dialog is a
/// structure viewer and mapping configurator, not an XML value editor.
/// </para>
///
/// <para>
/// Existing TBF mappings are shown directly beside their XML destinations.
/// Repeating prototype destinations can be used by multiple TBF results.
/// One-time destinations can be assigned only once.
/// </para>
/// </remarks>
public partial class XmlDestinationPickerDlg : Form
{
private readonly PayloadTemplateInspector inspector;
/// <summary>
/// Initializes a new picker dialog.
/// </summary>
public XmlDestinationPickerDlg()
{
InitializeComponent();
inspector =
new PayloadTemplateInspector();
try
{
Icon =
Properties.Resources.TBF_icon;
}
catch
{
// Optional icon only.
}
ConfiguredMappings =
new Dictionary<string, IList<string>>(
StringComparer.Ordinal);
}
/// <summary>
/// Gets or sets the customer XML example path.
/// </summary>
public string TemplatePath
{
get;
set;
}
/// <summary>
/// Gets or sets the TBF result currently being configured.
/// </summary>
public string CurrentSourceName
{
get;
set;
}
/// <summary>
/// Gets or sets the destination currently assigned to the edited item.
/// </summary>
public string CurrentDestinationPath
{
get;
set;
}
/// <summary>
/// Gets or sets mappings already configured by other TBF result items.
/// </summary>
/// <remarks>
/// Dictionary key is the logical XML destination. The list contains
/// TBF result names currently mapped to the destination.
/// </remarks>
public IDictionary<string, IList<string>> ConfiguredMappings
{
get;
set;
}
/// <summary>
/// Gets the selected logical destination.
/// </summary>
public string SelectedDestinationPath
{
get;
private set;
}
private void XmlDestinationPickerDlg_Load(
object sender,
EventArgs e)
{
Text =
"Select XML destination";
templatePathTextBox.Text =
TemplatePath ?? string.Empty;
configuringResultTextBox.Text =
CurrentSourceName ?? string.Empty;
selectedPathTextBox.Text =
string.Empty;
mappedResultsTextBox.Text =
string.Empty;
mappingStatusLabel.Text =
"Select an XML attribute or value.";
chooseButton.Enabled =
false;
LoadStructure();
}
/// <summary>
/// Loads the customer XML example and builds the structure tree.
/// </summary>
private void LoadStructure()
{
xmlTreeView.BeginUpdate();
try
{
xmlTreeView.Nodes.Clear();
PayloadTemplateInspectionResult result =
inspector.Inspect(
TemplatePath);
if (result.Root == null)
return;
TreeNode rootNode =
CreateTreeNode(
result.Root);
xmlTreeView.Nodes.Add(
rootNode);
rootNode.Expand();
TreeNode currentNode =
FindTreeNode(
xmlTreeView.Nodes,
CurrentDestinationPath);
if (currentNode != null)
{
ExpandParents(
currentNode);
xmlTreeView.SelectedNode =
currentNode;
currentNode.EnsureVisible();
}
}
catch (Exception exc)
{
MessageBox.Show(
this,
"Failed to analyze XML payload example." +
Environment.NewLine +
Environment.NewLine +
exc.Message,
"XML payload structure",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
finally
{
xmlTreeView.EndUpdate();
}
}
/// <summary>
/// Creates one visual tree node from the structural payload model.
/// </summary>
private TreeNode CreateTreeNode(
PayloadTemplateNode payloadNode)
{
string displayText =
payloadNode.DisplayText ??
payloadNode.Name ??
string.Empty;
IList<string> mappedBy =
GetConfiguredMappings(
payloadNode.DestinationPath);
bool isCurrentMapping =
payloadNode.IsSelectable &&
string.Equals(
payloadNode.DestinationPath,
CurrentDestinationPath,
StringComparison.Ordinal);
if (payloadNode.IsSelectable)
{
List<string> visibleMappings =
new List<string>();
if (mappedBy != null)
{
visibleMappings.AddRange(
mappedBy.Where(
value =>
!string.IsNullOrWhiteSpace(
value)));
}
if (isCurrentMapping &&
!string.IsNullOrWhiteSpace(
CurrentSourceName) &&
!visibleMappings.Contains(
CurrentSourceName))
{
visibleMappings.Insert(
0,
CurrentSourceName);
}
if (visibleMappings.Count > 0)
{
displayText +=
" <- " +
string.Join(
", ",
visibleMappings);
}
}
TreeNode treeNode =
new TreeNode(
displayText);
treeNode.Tag =
payloadNode;
ApplyMappingAppearance(
treeNode,
payloadNode,
mappedBy,
isCurrentMapping);
foreach (PayloadTemplateNode child
in payloadNode.Children)
{
treeNode.Nodes.Add(
CreateTreeNode(
child));
}
return treeNode;
}
/// <summary>
/// Applies mapping state colors and tooltips.
/// </summary>
private void ApplyMappingAppearance(
TreeNode treeNode,
PayloadTemplateNode payloadNode,
IList<string> mappedBy,
bool isCurrentMapping)
{
if (!payloadNode.IsSelectable)
{
if (payloadNode.IsRepeatingPrototypeRoot)
{
treeNode.NodeFont =
new Font(
xmlTreeView.Font,
FontStyle.Bold);
}
return;
}
if (isCurrentMapping)
{
treeNode.BackColor =
Color.LightBlue;
treeNode.ToolTipText =
"Current mapping for: " +
(CurrentSourceName ?? string.Empty);
return;
}
if (mappedBy != null &&
mappedBy.Count > 0)
{
if (payloadNode.DestinationKind ==
PayloadDestinationKind.RepeatingPrototype)
{
treeNode.BackColor =
Color.PaleGreen;
treeNode.ToolTipText =
"Repeating prototype destination. Mapped by: " +
string.Join(
", ",
mappedBy);
}
else
{
treeNode.BackColor =
Color.LightGoldenrodYellow;
treeNode.ToolTipText =
"One-time destination already mapped by: " +
string.Join(
", ",
mappedBy);
}
}
}
private void xmlTreeView_AfterSelect(
object sender,
TreeViewEventArgs e)
{
PayloadTemplateNode payloadNode =
e.Node != null
? e.Node.Tag as PayloadTemplateNode
: null;
UpdateSelectedNodeInformation(
payloadNode);
}
/// <summary>
/// Updates the detail section for the selected structural destination.
/// </summary>
private void UpdateSelectedNodeInformation(
PayloadTemplateNode payloadNode)
{
chooseButton.Enabled =
false;
selectedPathTextBox.Text =
string.Empty;
mappedResultsTextBox.Text =
string.Empty;
mappingStatusLabel.Text =
"Select an XML attribute or value.";
if (payloadNode == null ||
!payloadNode.IsSelectable)
{
return;
}
selectedPathTextBox.Text =
payloadNode.DestinationPath ??
string.Empty;
IList<string> mappedBy =
GetConfiguredMappings(
payloadNode.DestinationPath);
mappedResultsTextBox.Text =
mappedBy != null &&
mappedBy.Count > 0
? string.Join(
", ",
mappedBy)
: "Not mapped";
bool isCurrentMapping =
string.Equals(
payloadNode.DestinationPath,
CurrentDestinationPath,
StringComparison.Ordinal);
if (isCurrentMapping)
{
mappingStatusLabel.Text =
"Current mapping.";
chooseButton.Enabled =
true;
return;
}
if (payloadNode.DestinationKind ==
PayloadDestinationKind.RepeatingPrototype)
{
if (mappedBy != null &&
mappedBy.Count > 0)
{
mappingStatusLabel.Text =
"Repeating prototype destination. Multiple TBF results can use this mapping.";
}
else
{
mappingStatusLabel.Text =
"Repeating prototype destination.";
}
chooseButton.Enabled =
true;
return;
}
if (mappedBy != null &&
mappedBy.Count > 0)
{
mappingStatusLabel.Text =
"This one-time destination is already mapped.";
chooseButton.Enabled =
false;
return;
}
mappingStatusLabel.Text =
"Destination is available.";
chooseButton.Enabled =
true;
}
/// <summary>
/// Returns configured source names for a destination.
/// </summary>
private IList<string> GetConfiguredMappings(
string destinationPath)
{
if (ConfiguredMappings == null ||
string.IsNullOrWhiteSpace(
destinationPath))
{
return new List<string>();
}
foreach (KeyValuePair<string, IList<string>> pair
in ConfiguredMappings)
{
if (string.Equals(
pair.Key,
destinationPath,
StringComparison.Ordinal))
{
return pair.Value ??
new List<string>();
}
}
return new List<string>();
}
private void chooseButton_Click(
object sender,
EventArgs e)
{
ChooseSelectedDestination();
}
private void xmlTreeView_NodeMouseDoubleClick(
object sender,
TreeNodeMouseClickEventArgs e)
{
if (e.Node == null)
return;
xmlTreeView.SelectedNode =
e.Node;
if (chooseButton.Enabled)
{
ChooseSelectedDestination();
}
}
/// <summary>
/// Stores the selected destination and closes the dialog.
/// </summary>
private void ChooseSelectedDestination()
{
TreeNode selectedTreeNode =
xmlTreeView.SelectedNode;
if (selectedTreeNode == null)
return;
PayloadTemplateNode payloadNode =
selectedTreeNode.Tag
as PayloadTemplateNode;
if (payloadNode == null ||
!payloadNode.IsSelectable)
{
return;
}
SelectedDestinationPath =
payloadNode.DestinationPath;
DialogResult =
DialogResult.OK;
Close();
}
private TreeNode FindTreeNode(
TreeNodeCollection nodes,
string destinationPath)
{
if (string.IsNullOrWhiteSpace(
destinationPath))
{
return null;
}
foreach (TreeNode node
in nodes)
{
PayloadTemplateNode payloadNode =
node.Tag
as PayloadTemplateNode;
if (payloadNode != null &&
string.Equals(
payloadNode.DestinationPath,
destinationPath,
StringComparison.Ordinal))
{
return node;
}
TreeNode childResult =
FindTreeNode(
node.Nodes,
destinationPath);
if (childResult != null)
return childResult;
}
return null;
}
private void ExpandParents(
TreeNode node)
{
TreeNode parent =
node.Parent;
while (parent != null)
{
parent.Expand();
parent =
parent.Parent;
}
}
}
}