Files
tbf/TBF/Rig/Output/DataStorage/UniDataStorageWriter/UI/PayloadFileNameXmlTokenDlg.cs
T
marekf 57152ae749 <Feat>: Add XML-based payload archive filename configuration, increase revision to 3.9.3145.103
Cause:

- Payload archive filenames required values from the generated XML payload.
- Date/time values needed filename-safe formatting.
- XML result values such as Passed and Failed needed configurable conversion to P and F.

Solution:

1. Added XML filename tokens
   - Supports any unique non-repeating XML value using {Xml:/path}.
   - Resolves filename values directly from the generated payload.

2. Added user-friendly token configuration
   - Clicking inside a token opens the XML value chooser.
   - Added Time format and Mask transformations.

3. Improved filename generation
   - Validates XML paths and rejects ambiguous repeating values.
   - Sanitizes resolved values for safe filesystem usage.
   - Passes the per-meter payload context from ResultsWriter.

4. Integrated the new dialog
   - Added PayloadFileNameXmlTokenDlg and its Designer file.
   - Updated the TBF project configuration.

5. Increased revision
   - Updated revision to 3.9.3145.103.
2026-09-08 12:52:40 +02:00

513 lines
18 KiB
C#

///
/// Copyright (c) 2026 Sensus Slovensko a.s.
///
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters;
namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
{
/// <summary>
/// Selects one non-repeating XML payload destination for a filename token
/// and optionally configures a filename-safe value transformation.
/// </summary>
internal sealed partial class PayloadFileNameXmlTokenDlg : Form
{
private const string NoTransformation = "None";
private const string TimeFormatTransformation = "Time format";
private const string MaskTransformation = "Mask";
private readonly string templatePath;
private readonly string currentDestinationPath;
private readonly string currentTransformation;
private readonly TreeView xmlTreeView;
private readonly TextBox selectedPathTextBox;
private readonly ComboBox transformationComboBox;
private readonly ComboBox timeFormatComboBox;
private readonly Label timeFormatHintLabel;
private readonly Label maskHintLabel;
private readonly DataGridView maskGrid;
private readonly Button okButton;
public PayloadFileNameXmlTokenDlg(
string templatePath,
string currentDestinationPath,
string currentTransformation)
{
this.templatePath = templatePath;
this.currentDestinationPath = currentDestinationPath;
this.currentTransformation = currentTransformation;
Text = "Select XML value for payload filename";
StartPosition = FormStartPosition.CenterParent;
MinimumSize = new Size(760, 720);
Size = new Size(900, 760);
Label templateLabel = new Label
{
AutoSize = true,
Location = new Point(12, 16),
Text = "XML reference:"
};
TextBox templateTextBox = new TextBox
{
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
Location = new Point(120, 12),
ReadOnly = true,
Size = new Size(750, 20),
Text = templatePath ?? string.Empty
};
xmlTreeView = new TreeView
{
Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right,
HideSelection = false,
Location = new Point(12, 44),
ShowNodeToolTips = true,
Size = new Size(858, 400)
};
Label selectedPathLabel = new Label
{
Anchor = AnchorStyles.Bottom | AnchorStyles.Left,
AutoSize = true,
Location = new Point(12, 458),
Text = "Selected XML path:"
};
selectedPathTextBox = new TextBox
{
Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right,
Location = new Point(120, 454),
ReadOnly = true,
Size = new Size(750, 20)
};
Label transformationLabel = new Label
{
Anchor = AnchorStyles.Bottom | AnchorStyles.Left,
AutoSize = true,
Location = new Point(12, 492),
Text = "Transformation:"
};
transformationComboBox = new ComboBox
{
Anchor = AnchorStyles.Bottom | AnchorStyles.Left,
DropDownStyle = ComboBoxStyle.DropDownList,
Location = new Point(120, 488),
Size = new Size(220, 21)
};
transformationComboBox.Items.AddRange(new object[]
{
NoTransformation,
TimeFormatTransformation,
MaskTransformation
});
timeFormatComboBox = new ComboBox
{
Anchor = AnchorStyles.Bottom | AnchorStyles.Left,
DropDownStyle = ComboBoxStyle.DropDownList,
Location = new Point(120, 523),
Size = new Size(300, 21)
};
timeFormatComboBox.Items.AddRange(new object[]
{
"yyyyMMdd_HHmmss",
"yyyy-MM-dd_HH-mm-ss",
"yyyyMMdd-HHmmss",
"ddMMyyyy_HHmmss",
"yyyyMMdd_HHmm",
"yyyyMMdd"
});
timeFormatHintLabel = new Label
{
Anchor = AnchorStyles.Bottom | AnchorStyles.Left,
AutoSize = true,
ForeColor = SystemColors.GrayText,
Location = new Point(120, 550),
Text = "Only filename-safe formats are offered. Characters /, \\ and : are not allowed."
};
maskHintLabel = new Label
{
Anchor = AnchorStyles.Bottom | AnchorStyles.Left,
AutoSize = true,
ForeColor = SystemColors.GrayText,
Location = new Point(12, 523),
Text = "Value mask:"
};
maskGrid = new DataGridView
{
AllowUserToAddRows = true,
AllowUserToDeleteRows = true,
Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right,
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize,
Location = new Point(120, 523),
RowHeadersVisible = false,
Size = new Size(750, 130)
};
maskGrid.Columns.Add("SourceValue", "XML value");
maskGrid.Columns.Add("TargetValue", "Filename value");
okButton = new Button
{
Anchor = AnchorStyles.Bottom | AnchorStyles.Right,
Enabled = false,
Location = new Point(714, 674),
Size = new Size(75, 25),
Text = "OK"
};
Button cancelButton = new Button
{
Anchor = AnchorStyles.Bottom | AnchorStyles.Right,
DialogResult = DialogResult.Cancel,
Location = new Point(795, 674),
Size = new Size(75, 25),
Text = "Cancel"
};
Controls.Add(templateLabel);
Controls.Add(templateTextBox);
Controls.Add(xmlTreeView);
Controls.Add(selectedPathLabel);
Controls.Add(selectedPathTextBox);
Controls.Add(transformationLabel);
Controls.Add(transformationComboBox);
Controls.Add(timeFormatComboBox);
Controls.Add(timeFormatHintLabel);
Controls.Add(maskHintLabel);
Controls.Add(maskGrid);
Controls.Add(okButton);
Controls.Add(cancelButton);
AcceptButton = okButton;
CancelButton = cancelButton;
Load += PayloadFileNameXmlTokenDlg_Load;
xmlTreeView.AfterSelect += xmlTreeView_AfterSelect;
xmlTreeView.NodeMouseDoubleClick += xmlTreeView_NodeMouseDoubleClick;
transformationComboBox.SelectedIndexChanged += transformationComboBox_SelectedIndexChanged;
okButton.Click += okButton_Click;
}
public string SelectedDestinationPath { get; private set; }
public string Transformation
{
get
{
string selectedTransformation = transformationComboBox.SelectedItem as string;
if (string.Equals(selectedTransformation, TimeFormatTransformation, StringComparison.Ordinal))
{
string format = timeFormatComboBox.SelectedItem as string;
return string.IsNullOrWhiteSpace(format)
? string.Empty
: "timeformat:" + format;
}
if (string.Equals(selectedTransformation, MaskTransformation, StringComparison.Ordinal))
{
List<string> entries = new List<string>();
foreach (DataGridViewRow row in maskGrid.Rows)
{
if (row.IsNewRow)
continue;
string sourceValue = Convert.ToString(row.Cells[0].Value).Trim();
string targetValue = Convert.ToString(row.Cells[1].Value).Trim();
if (sourceValue.Length == 0 && targetValue.Length == 0)
continue;
entries.Add(sourceValue + "=" + targetValue);
}
return entries.Count == 0
? string.Empty
: "mask:" + string.Join(";", entries.ToArray());
}
return string.Empty;
}
}
private void PayloadFileNameXmlTokenDlg_Load(object sender, EventArgs e)
{
try
{
InitializeTransformation(currentTransformation);
PayloadTemplateInspectionResult inspection = new PayloadTemplateInspector().Inspect(templatePath);
if (inspection.Root == null)
return;
TreeNode root = CreateTreeNode(inspection.Root);
xmlTreeView.Nodes.Add(root);
root.Expand();
TreeNode currentNode = FindTreeNode(xmlTreeView.Nodes, currentDestinationPath);
if (currentNode != null)
{
ExpandParents(currentNode);
xmlTreeView.SelectedNode = currentNode;
currentNode.EnsureVisible();
}
}
catch (Exception ex)
{
MessageBox.Show(
this,
"Failed to analyze the XML reference." + Environment.NewLine + Environment.NewLine + ex.Message,
"XML filename value",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
}
private void InitializeTransformation(string transformation)
{
string definition = (transformation ?? string.Empty).Trim();
if (definition.StartsWith("timeformat:", StringComparison.OrdinalIgnoreCase))
{
SelectTimeFormat(definition.Substring("timeformat:".Length).Trim());
return;
}
if (definition.StartsWith("DateTime:", StringComparison.OrdinalIgnoreCase))
{
SelectTimeFormat(definition.Substring("DateTime:".Length).Trim());
return;
}
if (definition.StartsWith("mask:", StringComparison.OrdinalIgnoreCase))
{
SelectMask(definition.Substring("mask:".Length).Trim());
return;
}
if (definition.IndexOf('=') >= 0)
{
SelectMask(definition);
return;
}
if (definition.Length > 0)
{
SelectTimeFormat(definition);
return;
}
transformationComboBox.SelectedItem = NoTransformation;
}
private void SelectTimeFormat(string format)
{
transformationComboBox.SelectedItem = TimeFormatTransformation;
int formatIndex = timeFormatComboBox.FindStringExact(format);
timeFormatComboBox.SelectedIndex = formatIndex >= 0 ? formatIndex : 0;
}
private void SelectMask(string mapping)
{
transformationComboBox.SelectedItem = MaskTransformation;
string[] entries = mapping.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
foreach (string entry in entries)
{
int separator = entry.IndexOf('=');
if (separator <= 0)
continue;
maskGrid.Rows.Add(
entry.Substring(0, separator).Trim(),
entry.Substring(separator + 1).Trim());
}
}
private void transformationComboBox_SelectedIndexChanged(object sender, EventArgs e)
{
string selectedTransformation = transformationComboBox.SelectedItem as string;
bool showTimeFormat = string.Equals(
selectedTransformation,
TimeFormatTransformation,
StringComparison.Ordinal);
bool showMask = string.Equals(
selectedTransformation,
MaskTransformation,
StringComparison.Ordinal);
timeFormatComboBox.Visible = showTimeFormat;
timeFormatHintLabel.Visible = showTimeFormat;
maskHintLabel.Visible = showMask;
maskGrid.Visible = showMask;
if (showTimeFormat && timeFormatComboBox.SelectedIndex < 0)
timeFormatComboBox.SelectedIndex = 0;
}
private TreeNode CreateTreeNode(PayloadTemplateNode payloadNode)
{
TreeNode treeNode = new TreeNode(payloadNode.DisplayText ?? payloadNode.Name ?? string.Empty)
{
Tag = payloadNode
};
if (payloadNode.IsSelectable && payloadNode.DestinationKind == PayloadDestinationKind.RepeatingPrototype)
{
treeNode.ForeColor = SystemColors.GrayText;
treeNode.ToolTipText = "Repeating XML values cannot be used in a single filename.";
}
foreach (PayloadTemplateNode child in payloadNode.Children)
treeNode.Nodes.Add(CreateTreeNode(child));
return treeNode;
}
private void xmlTreeView_AfterSelect(object sender, TreeViewEventArgs e)
{
PayloadTemplateNode payloadNode = e.Node != null ? e.Node.Tag as PayloadTemplateNode : null;
bool selectable = payloadNode != null &&
payloadNode.IsSelectable &&
payloadNode.DestinationKind == PayloadDestinationKind.SingleValue;
selectedPathTextBox.Text = selectable ? payloadNode.DestinationPath : string.Empty;
okButton.Enabled = selectable;
}
private void xmlTreeView_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e)
{
if (e.Node != null)
xmlTreeView.SelectedNode = e.Node;
}
private void okButton_Click(object sender, EventArgs e)
{
AcceptSelection();
}
private void AcceptSelection()
{
maskGrid.EndEdit();
PayloadTemplateNode payloadNode = xmlTreeView.SelectedNode != null
? xmlTreeView.SelectedNode.Tag as PayloadTemplateNode
: null;
if (payloadNode == null ||
!payloadNode.IsSelectable ||
payloadNode.DestinationKind != PayloadDestinationKind.SingleValue)
{
return;
}
if (!ValidateSelectedTransformation())
return;
SelectedDestinationPath = payloadNode.DestinationPath;
DialogResult = DialogResult.OK;
Close();
}
private bool ValidateSelectedTransformation()
{
string selectedTransformation = transformationComboBox.SelectedItem as string;
if (!string.Equals(selectedTransformation, MaskTransformation, StringComparison.Ordinal))
return true;
int validRowCount = 0;
foreach (DataGridViewRow row in maskGrid.Rows)
{
if (row.IsNewRow)
continue;
string sourceValue = Convert.ToString(row.Cells[0].Value).Trim();
string targetValue = Convert.ToString(row.Cells[1].Value).Trim();
if (sourceValue.Length == 0 && targetValue.Length == 0)
continue;
if (sourceValue.Length == 0)
{
ShowMaskError("Enter the XML value to be replaced.");
return false;
}
if (sourceValue.IndexOfAny(new[] { '=', ';' }) >= 0 || targetValue.IndexOf(';') >= 0)
{
ShowMaskError("Mask values cannot contain semicolons. The XML value also cannot contain '='.");
return false;
}
validRowCount++;
}
if (validRowCount == 0)
{
ShowMaskError("Add at least one XML value and filename value pair.");
return false;
}
return true;
}
private void ShowMaskError(string message)
{
MessageBox.Show(
this,
message,
"XML filename mask",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
}
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 child = FindTreeNode(node.Nodes, destinationPath);
if (child != null)
return child;
}
return null;
}
private void ExpandParents(TreeNode node)
{
TreeNode parent = node.Parent;
while (parent != null)
{
parent.Expand();
parent = parent.Parent;
}
}
}
}