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.
513 lines
18 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|