/// /// 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 { /// /// Selects one non-repeating XML payload destination for a filename token /// and optionally configures a filename-safe value transformation. /// 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 entries = new List(); 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; } } } }