diff --git a/DeviceTest/app.config b/DeviceTest/app.config
index 4c9e351d5..b3dc11eca 100644
--- a/DeviceTest/app.config
+++ b/DeviceTest/app.config
@@ -7,6 +7,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/GenericTest/app.config b/GenericTest/app.config
index 5e7f0894f..48447540a 100644
--- a/GenericTest/app.config
+++ b/GenericTest/app.config
@@ -7,6 +7,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LabelPrinting/App.config b/LabelPrinting/App.config
index 1fa696586..be64bd998 100644
--- a/LabelPrinting/App.config
+++ b/LabelPrinting/App.config
@@ -9,6 +9,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ResetBatchNr/App.config b/ResetBatchNr/App.config
index 1fa696586..be64bd998 100644
--- a/ResetBatchNr/App.config
+++ b/ResetBatchNr/App.config
@@ -9,6 +9,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ResultsBrowser/app.config b/ResultsBrowser/app.config
index 6a3539d7e..6b44297a6 100644
--- a/ResultsBrowser/app.config
+++ b/ResultsBrowser/app.config
@@ -21,6 +21,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/S640TestApp/App.config b/S640TestApp/App.config
index 1fa696586..dd47cf82c 100644
--- a/S640TestApp/App.config
+++ b/S640TestApp/App.config
@@ -9,6 +9,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/TBF/Properties/AssemblyInfo.cs b/TBF/Properties/AssemblyInfo.cs
index 5c45b63fb..7d803eb22 100644
--- a/TBF/Properties/AssemblyInfo.cs
+++ b/TBF/Properties/AssemblyInfo.cs
@@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
// Build Number
// Revision
//
-[assembly: AssemblyVersion("3.9.3134.106")]
-[assembly: AssemblyFileVersion("3.9.3134.106")]
+[assembly: AssemblyVersion("3.9.3134.107")]
+[assembly: AssemblyFileVersion("3.9.3134.107")]
diff --git a/TBF/Rig/Output/DataStorage/UniDataStorageWriter/Interfaces/IDataStorageWriter.cs b/TBF/Rig/Output/DataStorage/UniDataStorageWriter/Interfaces/IDataStorageWriter.cs
index 40e733e9f..5746073f5 100644
--- a/TBF/Rig/Output/DataStorage/UniDataStorageWriter/Interfaces/IDataStorageWriter.cs
+++ b/TBF/Rig/Output/DataStorage/UniDataStorageWriter/Interfaces/IDataStorageWriter.cs
@@ -1,9 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Diagnostic;
+using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Diagnostic;
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Interfaces;
namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter
diff --git a/TBF/Rig/Output/DataStorage/UniDataStorageWriter/Types.cs b/TBF/Rig/Output/DataStorage/UniDataStorageWriter/Types.cs
index cebb0ae79..d1a5137d5 100644
--- a/TBF/Rig/Output/DataStorage/UniDataStorageWriter/Types.cs
+++ b/TBF/Rig/Output/DataStorage/UniDataStorageWriter/Types.cs
@@ -31,6 +31,7 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter
public const string Csv = ".csv";
public const string Xls = ".xls";
+ public const string Xlsx = ".xlsx";
public const string Json = ".json";
}
}
diff --git a/TBF/Rig/Output/DataStorage/UniDataStorageWriter/UI/WriterCfgCtrl.cs b/TBF/Rig/Output/DataStorage/UniDataStorageWriter/UI/WriterCfgCtrl.cs
index f7d16cd30..07f7c8131 100644
--- a/TBF/Rig/Output/DataStorage/UniDataStorageWriter/UI/WriterCfgCtrl.cs
+++ b/TBF/Rig/Output/DataStorage/UniDataStorageWriter/UI/WriterCfgCtrl.cs
@@ -1,12 +1,13 @@
- using Common;
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using TBF.Rig.Generic;
- using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Interfaces;
- using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Writers;
- using static TBF.Rig.Output.DataStorage.UniDataStorageWriter.Types;
+using Common;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using TBF.Rig.Generic;
+using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Interfaces;
+using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Writers;
+using static TBF.Rig.Output.DataStorage.UniDataStorageWriter.Types;
+using System.Runtime.InteropServices;
namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
{
@@ -25,6 +26,32 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
private WriterCfg config;
+ private bool isUnlocked;
+
+ private const string CsvTemplateHint =
+ "Write template is not used.";
+
+ private const string DefaultDatabaseInsertTemplate =
+ "Insert|INSERT INTO dbo.table ({0}) VALUES ({1})";
+
+ private const string DefaultDatabaseUpdateTemplate =
+ "Update|UPDATE dbo.table SET {2} = {3} WHERE {0} = {1}";
+
+ private const string DefaultXlsxInsertTemplate =
+ "Insert|INSERT INTO Sheet1 ({0}) VALUES ({1})";
+
+ private const string DefaultXlsxUpdateTemplate =
+ "Update|UPDATE Sheet1 SET {2} = {3} WHERE {0} = {1}";
+
+ private const int EmSetCueBanner = 0x1501;
+
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)]
+ private static extern IntPtr SendMessage(
+ IntPtr hWnd,
+ int msg,
+ IntPtr wParam,
+ string lParam);
+
public bool ShowMore
{
get { return false; }
@@ -55,6 +82,8 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
buttonUpdateTemplate.Click += buttonUpdateTemplate_Click;
buttonRemoveTemplate.Click += buttonRemoveTemplate_Click;
writeTemplatesListBox.SelectedIndexChanged += writeTemplatesListBox_SelectedIndexChanged;
+
+ technologyTypeComboBox.SelectedIndexChanged += technologyTypeComboBox_SelectedIndexChanged;
}
private void WriterCfgCtrl_Load(object sender, EventArgs e)
@@ -70,6 +99,8 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
if (config != null)
Redraw();
+
+ ApplyTechnologyConfiguration(true);
}
public void Closing()
@@ -118,29 +149,136 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
RefreshTemplatesListBox();
UpdateInputHint();
RefreshWriteParamsListBox();
+
+ ApplyTechnologyConfiguration(true);
}
public void Unlock()
{
+ isUnlocked = true;
+
nameTextBox.Enabled = true;
dataStorageTypeComboBox.Enabled = true;
dataSourceTextBox.Enabled = true;
+
sourceTestResultTextBox.Enabled = true;
sourceTestResultTextBox.ReadOnly = true;
+
writeTestResultTextBox.Enabled = true;
writeTestResultTextBox.ReadOnly = true;
+
writeParamValueTextBox.Enabled = true;
listBoxWriteParams.Enabled = true;
buttonAddParam.Enabled = true;
buttonRemoveParam.Enabled = true;
writeModeComboBox.Enabled = true;
technologyTypeComboBox.Enabled = true;
- writeTemplatesListBox.Enabled = true;
- templateEditTextBox.Enabled = true;
- buttonAddTemplate.Enabled = true;
- buttonUpdateTemplate.Enabled = true;
- buttonRemoveTemplate.Enabled = true;
- examples1Button.Enabled = true;
+
+ ApplyTechnologyConfiguration(false);
+ }
+
+ ///
+ /// Updates source hints, default templates and template control availability
+ /// according to the selected technology.
+ ///
+ ///
+ /// to create default templates when the template list
+ /// is empty; otherwise, .
+ ///
+ private void ApplyTechnologyConfiguration(bool initializeDefaults)
+ {
+ string technologyType =
+ (technologyTypeComboBox.Text ?? string.Empty).Trim();
+
+ SetDataSourceHint(GetDataSourceHint(technologyType));
+
+ bool templatesSupported =
+ technologyType == TechnologyTypes.MicrosoftSql ||
+ technologyType == TechnologyTypes.Xlsx ||
+ technologyType == TechnologyTypes.Xls;
+
+ if (technologyType == TechnologyTypes.Csv)
+ {
+ writeTemplatesListBox.Items.Clear();
+
+ templateEditTextBox.Text = CsvTemplateHint;
+
+ SetTemplateControlsEnabled(false);
+ return;
+ }
+
+ if (initializeDefaults &&
+ templatesSupported &&
+ writeTemplatesListBox.Items.Count == 0)
+ {
+ AddDefaultTemplates(technologyType);
+ }
+
+ if (!templatesSupported)
+ {
+ writeTemplatesListBox.Items.Clear();
+ templateEditTextBox.Clear();
+ }
+ else if (templateEditTextBox.Text == CsvTemplateHint)
+ {
+ templateEditTextBox.Clear();
+ }
+
+ SetTemplateControlsEnabled(
+ templatesSupported && isUnlocked);
+ }
+
+ ///
+ /// Adds default write templates for the selected technology.
+ ///
+ ///
+ /// Selected writer technology type.
+ ///
+ private void AddDefaultTemplates(string technologyType)
+ {
+ writeTemplatesListBox.Items.Clear();
+
+ switch (technologyType)
+ {
+ case TechnologyTypes.MicrosoftSql:
+ writeTemplatesListBox.Items.Add(
+ DefaultDatabaseInsertTemplate);
+
+ writeTemplatesListBox.Items.Add(
+ DefaultDatabaseUpdateTemplate);
+ break;
+
+ case TechnologyTypes.Xlsx:
+ case TechnologyTypes.Xls:
+ writeTemplatesListBox.Items.Add(
+ DefaultXlsxInsertTemplate);
+
+ writeTemplatesListBox.Items.Add(
+ DefaultXlsxUpdateTemplate);
+ break;
+ }
+
+ if (writeTemplatesListBox.Items.Count > 0)
+ writeTemplatesListBox.SelectedIndex = 0;
+ }
+
+ ///
+ /// Enables or disables controls used for write template configuration.
+ ///
+ ///
+ /// to enable template editing; otherwise,
+ /// .
+ ///
+ private void SetTemplateControlsEnabled(bool enabled)
+ {
+ writeTemplatesListBox.Enabled = enabled;
+ templateEditTextBox.Enabled = enabled;
+
+ buttonAddTemplate.Enabled = enabled;
+ buttonUpdateTemplate.Enabled = enabled;
+ buttonRemoveTemplate.Enabled = enabled;
+
+ examples1Button.Enabled = enabled;
}
public CfgUpdateFlags VerifyCfg(ref string message)
@@ -214,14 +352,9 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
tmpCfg.TechnologyType = (technologyTypeComboBox.Text ?? string.Empty).Trim();
tmpCfg.DataSource = dataSourceTextBox.Text;
- if (config != null && config.WriteTemplates != null)
- {
- tmpCfg.WriteTemplates = new List(config.WriteTemplates);
- }
- else
- {
- tmpCfg.WriteTemplates = new List();
- }
+ tmpCfg.WriteTemplates = writeTemplatesListBox.Items
+ .Cast()
+ .ToList();
string selectedTemplate = tmpCfg.GetTemplate(mode);
tmpCfg.QueryTemplate = selectedTemplate;
@@ -267,6 +400,9 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
case TechnologyTypes.Csv:
return new CsvWriter(cfg);
+ case TechnologyTypes.Xlsx:
+ return new XlsxWriter(cfg);
+
case TechnologyTypes.Json:
throw new NotSupportedException("JSON writer is not supported yet.");
@@ -362,18 +498,39 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
return request;
}
+ ///
+ /// Parses an insert parameter in the format Column=Value.
+ ///
+ ///
+ /// Text containing the column name and value.
+ ///
+ ///
+ /// Parsed insert write item.
+ ///
+ ///
+ /// Thrown when the input format is invalid.
+ ///
private InsertWriteItem ParseInsertItem(string text)
{
if (string.IsNullOrWhiteSpace(text))
throw new InvalidOperationException("Insert item is empty.");
string[] parts = text
- .Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
+ .Split(new[] { '=' }, 2, StringSplitOptions.None)
.Select(p => p.Trim())
.ToArray();
if (parts.Length != 2)
- throw new InvalidOperationException("Insert format must be: Column;Value");
+ throw new InvalidOperationException(
+ "Insert format must be: Column=Value");
+
+ if (string.IsNullOrWhiteSpace(parts[0]))
+ throw new InvalidOperationException(
+ "Insert column name cannot be empty.");
+
+ if (string.IsNullOrWhiteSpace(parts[1]))
+ throw new InvalidOperationException(
+ "Insert value cannot be empty.");
return new InsertWriteItem
{
@@ -382,25 +539,75 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
};
}
+ ///
+ /// Parses an update parameter in the format
+ /// WhereName=WhereValue;SetName=SetValue.
+ ///
+ ///
+ /// Text containing the update condition and target value.
+ ///
+ ///
+ /// Parsed update write item.
+ ///
+ ///
+ /// Thrown when the input format is invalid.
+ ///
private UpdateWriteItem ParseUpdateItem(string text)
{
if (string.IsNullOrWhiteSpace(text))
throw new InvalidOperationException("Update item is empty.");
- string[] parts = text
- .Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
+ string[] pairs = text
+ .Split(new[] { ';' }, StringSplitOptions.None)
.Select(p => p.Trim())
.ToArray();
- if (parts.Length != 4)
- throw new InvalidOperationException("Update format must be: WhereName;WhereValue;SetName;SetValue");
+ if (pairs.Length != 2)
+ {
+ throw new InvalidOperationException(
+ "Update format must be: " +
+ "WhereName=WhereValue;SetName=SetValue");
+ }
+
+ string[] whereParts = pairs[0]
+ .Split(new[] { '=' }, 2, StringSplitOptions.None)
+ .Select(p => p.Trim())
+ .ToArray();
+
+ string[] setParts = pairs[1]
+ .Split(new[] { '=' }, 2, StringSplitOptions.None)
+ .Select(p => p.Trim())
+ .ToArray();
+
+ if (whereParts.Length != 2 || setParts.Length != 2)
+ {
+ throw new InvalidOperationException(
+ "Update format must be: " +
+ "WhereName=WhereValue;SetName=SetValue");
+ }
+
+ if (string.IsNullOrWhiteSpace(whereParts[0]))
+ throw new InvalidOperationException(
+ "WHERE column name cannot be empty.");
+
+ if (string.IsNullOrWhiteSpace(whereParts[1]))
+ throw new InvalidOperationException(
+ "WHERE value cannot be empty.");
+
+ if (string.IsNullOrWhiteSpace(setParts[0]))
+ throw new InvalidOperationException(
+ "SET column name cannot be empty.");
+
+ if (string.IsNullOrWhiteSpace(setParts[1]))
+ throw new InvalidOperationException(
+ "SET value cannot be empty.");
return new UpdateWriteItem
{
- WhereParameterName = parts[0],
- WhereValue = parts[1],
- SetParameterName = parts[2],
- SetValue = parts[3]
+ WhereParameterName = whereParts[0],
+ WhereValue = whereParts[1],
+ SetParameterName = setParts[0],
+ SetValue = setParts[1]
};
}
@@ -413,18 +620,33 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
if (string.IsNullOrWhiteSpace(text))
return;
- WriteMode mode;
- if (!Enum.TryParse(writeModeComboBox.Text, true, out mode))
- mode = WriteMode.Insert;
+ try
+ {
+ WriteMode mode;
- if (mode == WriteMode.Insert)
- ParseInsertItem(text);
- else if (mode == WriteMode.Update)
- ParseUpdateItem(text);
+ if (!Enum.TryParse(writeModeComboBox.Text, true, out mode))
+ mode = WriteMode.Insert;
- batchWriteLines.Add(text);
- writeParamValueTextBox.Clear();
- RefreshWriteParamsListBox();
+ if (mode == WriteMode.Insert)
+ ParseInsertItem(text);
+ else if (mode == WriteMode.Update)
+ ParseUpdateItem(text);
+
+ batchWriteLines.Add(text);
+
+ writeParamValueTextBox.Clear();
+ RefreshWriteParamsListBox();
+ }
+ catch (InvalidOperationException ex)
+ {
+ writeTestResultTextBox.Text =
+ "Invalid write parameter:" +
+ Environment.NewLine +
+ ex.Message;
+
+ writeParamValueTextBox.Focus();
+ writeParamValueTextBox.SelectAll();
+ }
}
private void buttonRemoveParam_Click(object sender, EventArgs e)
@@ -452,13 +674,16 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
{
if (writeModeComboBox.Text == WriteMode.Insert.ToString())
{
- labelParamValue.Text = "Insert: Column;Value";
- writeParamValueTextBox.Text = "Column;Value";
+ labelParamValue.Text = "Insert: Column=Value";
+ writeParamValueTextBox.Text = "Column=Value";
}
else
{
- labelParamValue.Text = "Update: WhereName;WhereValue;SetName;SetValue";
- writeParamValueTextBox.Text = "WhereName;WhereValue;SetName;SetValue";
+ labelParamValue.Text =
+ "Update: WhereName=WhereValue;SetName=SetValue";
+
+ writeParamValueTextBox.Text =
+ "WhereName=WhereValue;SetName=SetValue";
}
}
@@ -539,37 +764,64 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
private void UpdateTechnologyTypeUi()
{
- string storageType = (dataStorageTypeComboBox.Text ?? string.Empty).Trim();
+ string previousTechnology =
+ (technologyTypeComboBox.Text ?? string.Empty).Trim();
+
+ string storageType =
+ (dataStorageTypeComboBox.Text ?? string.Empty).Trim();
technologyTypeComboBox.Items.Clear();
- technologyTypeComboBox.Text = string.Empty;
switch (storageType)
{
case StorageTypes.LocalDatabase:
case StorageTypes.RemoteDatabase:
technologyTypeLabel.Text = "Database type:";
- technologyTypeComboBox.Items.Add(TechnologyTypes.MicrosoftSql);
- technologyTypeComboBox.Items.Add(TechnologyTypes.MySqlMariaDb);
- technologyTypeComboBox.Items.Add(TechnologyTypes.SQLite);
+
+ technologyTypeComboBox.Items.Add(
+ TechnologyTypes.MicrosoftSql);
+
+ technologyTypeComboBox.Items.Add(
+ TechnologyTypes.MySqlMariaDb);
+
+ technologyTypeComboBox.Items.Add(
+ TechnologyTypes.SQLite);
break;
case StorageTypes.LocalFile:
case StorageTypes.RemoteFile:
technologyTypeLabel.Text = "File type:";
- technologyTypeComboBox.Items.Add(TechnologyTypes.Csv);
- technologyTypeComboBox.Items.Add(TechnologyTypes.Xls);
- technologyTypeComboBox.Items.Add(TechnologyTypes.Json);
- break;
- case StorageTypes.RestApi:
- technologyTypeLabel.Text = "Technology type:";
+ technologyTypeComboBox.Items.Add(
+ TechnologyTypes.Csv);
+
+ technologyTypeComboBox.Items.Add(
+ TechnologyTypes.Xlsx);
+
+ technologyTypeComboBox.Items.Add(
+ TechnologyTypes.Xls);
+
+ technologyTypeComboBox.Items.Add(
+ TechnologyTypes.Json);
break;
default:
technologyTypeLabel.Text = "Technology type:";
break;
}
+
+ if (!string.IsNullOrWhiteSpace(previousTechnology) &&
+ technologyTypeComboBox.Items.Contains(previousTechnology))
+ {
+ technologyTypeComboBox.SelectedItem =
+ previousTechnology;
+ }
+ else if (technologyTypeComboBox.Items.Count > 0)
+ {
+ technologyTypeComboBox.SelectedIndex = 0;
+ }
+
+ ApplyTechnologyConfiguration(true);
}
private void writeModeComboBox_SelectedIndexChanged(object sender, EventArgs e)
@@ -599,6 +851,11 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
templateEditTextBox.Text = writeTemplatesListBox.SelectedItem.ToString();
}
+ private void technologyTypeComboBox_SelectedIndexChanged(object sender, EventArgs e)
+ {
+ ApplyTechnologyConfiguration(true);
+ }
+
private void buttonAddTemplate_Click(object sender, EventArgs e)
{
string text = (templateEditTextBox.Text ?? string.Empty).Trim();
@@ -652,5 +909,55 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
writeTemplatesListBox.SelectedIndex = newIndex;
}
}
+
+ ///
+ /// Returns the data source hint for the selected technology.
+ ///
+ ///
+ /// Selected technology type.
+ ///
+ ///
+ /// Data source input hint.
+ ///
+ private string GetDataSourceHint(string technologyType)
+ {
+ switch (technologyType)
+ {
+ case TechnologyTypes.Csv:
+ return "Insert the .csv file path";
+
+ case TechnologyTypes.Xlsx:
+ return "Insert the .xlsx file path";
+
+ case TechnologyTypes.Xls:
+ return "Insert the .xls file path";
+
+ case TechnologyTypes.MicrosoftSql:
+ case TechnologyTypes.MySqlMariaDb:
+ case TechnologyTypes.SQLite:
+ return "Insert database connection string";
+
+ default:
+ return "Insert data storage source";
+ }
+ }
+
+ ///
+ /// Sets the placeholder text displayed by the data source textbox.
+ ///
+ ///
+ /// Placeholder text.
+ ///
+ private void SetDataSourceHint(string hint)
+ {
+ if (!dataSourceTextBox.IsHandleCreated)
+ return;
+
+ SendMessage(
+ dataSourceTextBox.Handle,
+ EmSetCueBanner,
+ new IntPtr(1),
+ hint ?? string.Empty);
+ }
}
}
\ No newline at end of file
diff --git a/TBF/Rig/Output/DataStorage/UniDataStorageWriter/UI/WriterCfgCtrl.designer.cs b/TBF/Rig/Output/DataStorage/UniDataStorageWriter/UI/WriterCfgCtrl.designer.cs
index ed9bbded4..4556d06de 100644
--- a/TBF/Rig/Output/DataStorage/UniDataStorageWriter/UI/WriterCfgCtrl.designer.cs
+++ b/TBF/Rig/Output/DataStorage/UniDataStorageWriter/UI/WriterCfgCtrl.designer.cs
@@ -202,7 +202,6 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.UI
this.writeDataByParamAndTemplateButton.TabIndex = 17;
this.writeDataByParamAndTemplateButton.Text = "Write data by param and template";
this.writeDataByParamAndTemplateButton.UseVisualStyleBackColor = true;
- this.writeDataByParamAndTemplateButton.Click += new System.EventHandler(this.writeDataByParamAndTemplateButton_Click);
//
// groupBox3
//
diff --git a/TBF/Rig/Output/DataStorage/UniDataStorageWriter/Writer.cs b/TBF/Rig/Output/DataStorage/UniDataStorageWriter/Writer.cs
index 759bcf15d..e01d0deef 100644
--- a/TBF/Rig/Output/DataStorage/UniDataStorageWriter/Writer.cs
+++ b/TBF/Rig/Output/DataStorage/UniDataStorageWriter/Writer.cs
@@ -105,6 +105,12 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter
case TechnologyTypes.Csv:
return new CsvWriter(cfg);
+ case TechnologyTypes.Xlsx:
+ return new XlsxWriter(cfg);
+
+ case TechnologyTypes.Xls:
+ return new XlsxWriter(cfg);
+
case TechnologyTypes.Json:
return new JsonWriter(cfg);
diff --git a/TBF/Rig/Output/DataStorage/UniDataStorageWriter/Writers/CsvWriter .cs b/TBF/Rig/Output/DataStorage/UniDataStorageWriter/Writers/CsvWriter .cs
index abaf7333e..f1a55ddb0 100644
--- a/TBF/Rig/Output/DataStorage/UniDataStorageWriter/Writers/CsvWriter .cs
+++ b/TBF/Rig/Output/DataStorage/UniDataStorageWriter/Writers/CsvWriter .cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
@@ -99,30 +100,160 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Writers
}
///
- /// Insert mode:
- /// request.InsertItems define a single CSV row.
- /// Example:
- /// SerialNumber=SN001
- /// Result=PASS
- /// Produces:
- /// SN001;PASS
- ///
- /// Current implementation writes only values, not header names.
+ /// Appends one data row to the CSV file.
///
+ ///
+ /// Request containing the insert items.
+ ///
+ ///
+ /// A diagnostic result describing the inserted row.
+ ///
+ ///
+ ///
+ /// When the CSV file is empty or contains only empty lines, the first row is
+ /// automatically created from the insert item column names.
+ ///
+ ///
+ /// When the CSV file already contains a header, values are ordered according
+ /// to the existing header.
+ ///
+ ///
private WriterDiagnosticResult ExecuteInsert(DataWriteRequest request)
{
- if (request.InsertItems == null || request.InsertItems.Count == 0)
+ if (request.InsertItems == null ||
+ request.InsertItems.Count == 0)
+ {
return Fail("No insert items provided.");
+ }
- string delimiter = ";";
+ if (request.InsertItems.Any(i => i == null))
+ return Fail("Insert items contain a null item.");
- string[] values = request.InsertItems
- .Select(i => EscapeCsvValue(i != null ? i.Value : null))
+ if (request.InsertItems.Any(
+ i => string.IsNullOrWhiteSpace(i.ColumnName)))
+ {
+ return Fail("Insert item contains an empty column name.");
+ }
+
+ string[] duplicateColumns = request.InsertItems
+ .GroupBy(
+ i => i.ColumnName,
+ StringComparer.OrdinalIgnoreCase)
+ .Where(g => g.Count() > 1)
+ .Select(g => g.Key)
+ .ToArray();
+
+ if (duplicateColumns.Length > 0)
+ {
+ return Fail(
+ "Insert items contain duplicate columns: " +
+ string.Join(", ", duplicateColumns));
+ }
+
+ const string delimiter = ";";
+
+ Dictionary valuesByColumn =
+ request.InsertItems.ToDictionary(
+ item => item.ColumnName,
+ item => item.Value,
+ StringComparer.OrdinalIgnoreCase);
+
+ string firstNonEmptyLine = null;
+
+ if (File.Exists(cfg.DataSource))
+ {
+ firstNonEmptyLine = File
+ .ReadLines(cfg.DataSource, Encoding.UTF8)
+ .FirstOrDefault(line => !string.IsNullOrWhiteSpace(line));
+ }
+
+ string[] headerColumns;
+
+ if (string.IsNullOrWhiteSpace(firstNonEmptyLine))
+ {
+ headerColumns = request.InsertItems
+ .Select(item => item.ColumnName)
+ .ToArray();
+
+ string headerLine = string.Join(
+ delimiter,
+ headerColumns.Select(EscapeCsvValue));
+
+ // The file is empty or contains only blank lines.
+ // Rewrite it so that the header is always the first row.
+ File.WriteAllText(
+ cfg.DataSource,
+ headerLine + Environment.NewLine,
+ Encoding.UTF8);
+ }
+ else
+ {
+ headerColumns = ParseCsvLine(
+ firstNonEmptyLine,
+ delimiter[0])
+ .Select(value => value.Trim())
+ .ToArray();
+
+ if (headerColumns.Length == 0 ||
+ headerColumns.All(string.IsNullOrWhiteSpace))
+ {
+ return Fail("CSV file does not contain a valid header.");
+ }
+
+ if (headerColumns.Any(string.IsNullOrWhiteSpace))
+ {
+ return Fail(
+ "CSV header contains an empty column name.");
+ }
+
+ string[] duplicateHeaderColumns = headerColumns
+ .GroupBy(
+ name => name,
+ StringComparer.OrdinalIgnoreCase)
+ .Where(g => g.Count() > 1)
+ .Select(g => g.Key)
+ .ToArray();
+
+ if (duplicateHeaderColumns.Length > 0)
+ {
+ return Fail(
+ "CSV header contains duplicate columns: " +
+ string.Join(", ", duplicateHeaderColumns));
+ }
+ }
+
+ string[] missingColumns = request.InsertItems
+ .Select(item => item.ColumnName)
+ .Where(name => !headerColumns.Contains(
+ name,
+ StringComparer.OrdinalIgnoreCase))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+
+ if (missingColumns.Length > 0)
+ {
+ return Fail(
+ "CSV header does not contain columns: " +
+ string.Join(", ", missingColumns));
+ }
+
+ string[] values = headerColumns
+ .Select(columnName =>
+ {
+ string value;
+
+ return valuesByColumn.TryGetValue(columnName, out value)
+ ? EscapeCsvValue(value)
+ : string.Empty;
+ })
.ToArray();
string line = string.Join(delimiter, values);
- File.AppendAllText(cfg.DataSource, line + Environment.NewLine, Encoding.UTF8);
+ File.AppendAllText(
+ cfg.DataSource,
+ line + Environment.NewLine,
+ Encoding.UTF8);
return new WriterDiagnosticResult
{
@@ -132,6 +263,76 @@ namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Writers
};
}
+ ///
+ /// Parses a single CSV line while respecting quoted values.
+ ///
+ ///
+ /// CSV line to parse.
+ ///
+ ///
+ /// Character separating individual fields.
+ ///
+ ///
+ /// Parsed CSV field values.
+ ///
+ ///
+ /// Thrown when the line contains an unterminated quoted value.
+ ///
+ private string[] ParseCsvLine(string line, char delimiter)
+ {
+ if (line == null)
+ return new string[0];
+
+ List values = new List();
+ StringBuilder currentValue = new StringBuilder();
+
+ bool insideQuotes = false;
+
+ for (int index = 0; index < line.Length; index++)
+ {
+ char character = line[index];
+
+ if (character == '"')
+ {
+ bool escapedQuote =
+ insideQuotes &&
+ index + 1 < line.Length &&
+ line[index + 1] == '"';
+
+ if (escapedQuote)
+ {
+ currentValue.Append('"');
+ index++;
+ }
+ else
+ {
+ insideQuotes = !insideQuotes;
+ }
+
+ continue;
+ }
+
+ if (character == delimiter && !insideQuotes)
+ {
+ values.Add(currentValue.ToString());
+ currentValue.Clear();
+ continue;
+ }
+
+ currentValue.Append(character);
+ }
+
+ if (insideQuotes)
+ {
+ throw new InvalidOperationException(
+ "CSV line contains an unterminated quoted value.");
+ }
+
+ values.Add(currentValue.ToString());
+
+ return values.ToArray();
+ }
+
///
/// Update mode:
/// CSV has no natural SQL-style row update, so current implementation writes
diff --git a/TBF/Rig/Output/DataStorage/UniDataStorageWriter/Writers/XlsxWriter.cs b/TBF/Rig/Output/DataStorage/UniDataStorageWriter/Writers/XlsxWriter.cs
new file mode 100644
index 000000000..a82e21fc8
--- /dev/null
+++ b/TBF/Rig/Output/DataStorage/UniDataStorageWriter/Writers/XlsxWriter.cs
@@ -0,0 +1,1032 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using ClosedXML.Excel;
+using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Diagnostic;
+using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Interfaces;
+using static TBF.Rig.Output.DataStorage.UniDataStorageWriter.Types;
+
+namespace TBF.Rig.Output.DataStorage.UniDataStorageWriter.Writers
+{
+ ///
+ /// Provides data writing support for Microsoft Excel Open XML files.
+ ///
+ ///
+ ///
+ /// The writer supports files using the .xlsx extension.
+ /// Microsoft Excel does not need to be installed on the target computer.
+ ///
+ ///
+ /// The first row of the first worksheet is used as the column header row.
+ /// Insert operations append a new row to the worksheet. Update operations
+ /// locate rows by a specified column value and update the requested cell.
+ ///
+ ///
+ public class XlsxWriter : IDataStorageWriter
+ {
+ private const string DefaultWorksheetName = "Data";
+
+ private readonly WriterCfg cfg;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// Writer configuration containing the path of the target Excel file.
+ ///
+ ///
+ /// Thrown when is .
+ ///
+ public XlsxWriter(WriterCfg cfg)
+ {
+ this.cfg = cfg ?? throw new ArgumentNullException(nameof(cfg));
+ }
+
+ ///
+ /// Gets the storage types, technology types and write modes supported
+ /// by this writer.
+ ///
+ ///
+ /// The capabilities of the XLSX writer.
+ ///
+ public WriterCapabilities Capabilities
+ {
+ get
+ {
+ WriterCapabilities caps = new WriterCapabilities();
+
+ caps.SupportedStorageTypes.Add(StorageTypes.LocalFile);
+ caps.SupportedStorageTypes.Add(StorageTypes.RemoteFile);
+
+ caps.SupportedTechnologyTypes.Add(TechnologyTypes.Xlsx);
+
+ caps.SupportedWriteModes.Add(WriteMode.Insert);
+ caps.SupportedWriteModes.Add(WriteMode.Update);
+
+ return caps;
+ }
+ }
+
+ ///
+ /// Validates the configured XLSX source.
+ ///
+ ///
+ /// to validate the configured path without creating
+ /// missing directories or files; otherwise, .
+ ///
+ ///
+ /// A diagnostic result describing whether the XLSX source is valid
+ /// and accessible.
+ ///
+ ///
+ /// When is ,
+ /// a missing directory and XLSX file are created automatically.
+ ///
+ public WriterDiagnosticResult TestSource(bool validateOnly)
+ {
+ try
+ {
+ WriterDiagnosticResult pathValidation = ValidateDataSource();
+ if (!pathValidation.Success)
+ return pathValidation;
+
+ string path = cfg.DataSource;
+ string directory = Path.GetDirectoryName(path);
+
+ if (!string.IsNullOrWhiteSpace(directory) &&
+ !Directory.Exists(directory))
+ {
+ if (validateOnly)
+ return Fail("Directory does not exist: " + directory);
+
+ Directory.CreateDirectory(directory);
+ }
+
+ if (!File.Exists(path))
+ {
+ if (validateOnly)
+ return Ok("XLSX file does not exist but the path is valid.");
+
+ CreateEmptyWorkbook(path);
+
+ return Ok("XLSX source was created successfully.");
+ }
+
+ using (XLWorkbook workbook = new XLWorkbook(path))
+ {
+ if (!workbook.Worksheets.Any())
+ return Fail("XLSX file does not contain any worksheet.");
+
+ IXLWorksheet worksheet = workbook.Worksheet(1);
+
+ if (worksheet == null)
+ return Fail("The first XLSX worksheet could not be opened.");
+ }
+
+ return Ok("XLSX source is ready.");
+ }
+ catch (IOException ex)
+ {
+ return Fail(
+ "XLSX source cannot be accessed. The file may be opened " +
+ "or locked by another process: " + ex.Message);
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ return Fail(
+ "Access to the XLSX source was denied: " + ex.Message);
+ }
+ catch (Exception ex)
+ {
+ return Fail("XLSX source test failed: " + ex.Message);
+ }
+ }
+
+ ///
+ /// Writes data to the configured XLSX file.
+ ///
+ ///
+ /// Request containing the write mode and the data to be written.
+ ///
+ ///
+ /// A diagnostic result describing the outcome of the write operation.
+ ///
+ ///
+ /// Thrown when is .
+ ///
+ public WriterDiagnosticResult WriteData(DataWriteRequest request)
+ {
+ if (request == null)
+ throw new ArgumentNullException(nameof(request));
+
+ WriterDiagnosticResult sourceTest = TestSource(false);
+ if (!sourceTest.Success)
+ return sourceTest;
+
+ try
+ {
+ switch (request.Mode)
+ {
+ case WriteMode.Insert:
+ return ExecuteInsert(request);
+
+ case WriteMode.Update:
+ return ExecuteUpdate(request);
+
+ default:
+ return Fail(
+ "XLSX mode is not supported: " + request.Mode);
+ }
+ }
+ catch (IOException ex)
+ {
+ return Fail(
+ "XLSX file cannot be written. The file may be opened " +
+ "or locked by another process: " + ex.Message);
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ return Fail(
+ "Access to the XLSX file was denied: " + ex.Message);
+ }
+ catch (Exception ex)
+ {
+ return Fail("XLSX write failed: " + ex.Message);
+ }
+ }
+
+ ///
+ /// Appends one data row to the first worksheet.
+ ///
+ ///
+ /// Request containing the insert items.
+ ///
+ ///
+ /// A diagnostic result describing the inserted row.
+ ///
+ ///
+ ///
+ /// When the worksheet does not yet contain headers, the first row is
+ /// automatically created from the insert item column names.
+ ///
+ ///
+ /// When headers already exist, every insert item column must be present
+ /// in the header row.
+ ///
+ ///
+ private WriterDiagnosticResult ExecuteInsert(DataWriteRequest request)
+ {
+ if (request.InsertItems == null ||
+ request.InsertItems.Count == 0)
+ {
+ return Fail("No insert items provided.");
+ }
+
+ if (request.InsertItems.Any(i => i == null))
+ return Fail("Insert items contain a null item.");
+
+ if (request.InsertItems.Any(
+ i => string.IsNullOrWhiteSpace(i.ColumnName)))
+ {
+ return Fail("Insert item contains an empty column name.");
+ }
+
+ string[] duplicateColumns = request.InsertItems
+ .GroupBy(
+ i => i.ColumnName,
+ StringComparer.OrdinalIgnoreCase)
+ .Where(g => g.Count() > 1)
+ .Select(g => g.Key)
+ .ToArray();
+
+ if (duplicateColumns.Length > 0)
+ {
+ return Fail(
+ "Insert items contain duplicate columns: " +
+ string.Join(", ", duplicateColumns));
+ }
+
+ using (XLWorkbook workbook = OpenWorkbook())
+ {
+ string template = cfg.GetTemplate(request.Mode);
+ string worksheetName = ParseWorksheetName(template);
+
+ IXLWorksheet worksheet = GetOrCreateWorksheet(
+ workbook,
+ worksheetName);
+
+ Dictionary columns =
+ GetOrCreateHeaderColumns(
+ worksheet,
+ request.InsertItems.Select(i => i.ColumnName));
+
+ string[] missingColumns = request.InsertItems
+ .Select(i => i.ColumnName)
+ .Where(name => !columns.ContainsKey(name))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+
+ if (missingColumns.Length > 0)
+ {
+ return Fail(
+ "XLSX worksheet does not contain columns: " +
+ string.Join(", ", missingColumns));
+ }
+
+ int targetRowNumber = GetNextDataRowNumber(worksheet);
+ IXLRow targetRow = worksheet.Row(targetRowNumber);
+
+ foreach (InsertWriteItem item in request.InsertItems)
+ {
+ int columnNumber = columns[item.ColumnName];
+
+ SetCellValue(
+ targetRow.Cell(columnNumber),
+ item.Value);
+ }
+
+ workbook.Save();
+
+ string diagnostic = BuildInsertDiagnostic(
+ worksheet.Name,
+ targetRowNumber,
+ request.InsertItems);
+
+ return new WriterDiagnosticResult
+ {
+ Success = true,
+ Message = "XLSX insert OK. Written 1 row.",
+ ExecutedTemplate = diagnostic
+ };
+ }
+ }
+
+ ///
+ /// Updates cells in rows matching the specified conditions.
+ ///
+ ///
+ /// Request containing the update items.
+ ///
+ ///
+ /// A diagnostic result containing the number of updated rows.
+ ///
+ ///
+ /// Each update item is processed separately. All rows whose value in
+ /// equals
+ /// are updated.
+ /// String comparison is case-sensitive.
+ ///
+ private WriterDiagnosticResult ExecuteUpdate(DataWriteRequest request)
+ {
+ if (request.UpdateItems == null ||
+ request.UpdateItems.Count == 0)
+ {
+ return Fail("No update items provided.");
+ }
+
+ if (request.UpdateItems.Any(i => i == null))
+ return Fail("Update items contain a null item.");
+
+ using (XLWorkbook workbook = OpenWorkbook())
+ {
+ string template = cfg.GetTemplate(request.Mode);
+ string worksheetName = ParseWorksheetName(template);
+
+ IXLWorksheet worksheet = GetOrCreateWorksheet(
+ workbook,
+ worksheetName);
+
+ Dictionary columns =
+ GetHeaderColumns(worksheet);
+
+ if (columns.Count == 0)
+ return Fail("XLSX worksheet does not contain a header row.");
+
+ int totalUpdatedRows = 0;
+ StringBuilder diagnostics = new StringBuilder();
+
+ foreach (UpdateWriteItem item in request.UpdateItems)
+ {
+ WriterDiagnosticResult validation =
+ ValidateUpdateItem(item, columns);
+
+ if (!validation.Success)
+ return validation;
+
+ int whereColumnNumber =
+ columns[item.WhereParameterName];
+
+ int setColumnNumber =
+ columns[item.SetParameterName];
+
+ int updatedRows = UpdateMatchingRows(
+ worksheet,
+ whereColumnNumber,
+ item.WhereValue,
+ setColumnNumber,
+ item.SetValue);
+
+ totalUpdatedRows += updatedRows;
+
+ diagnostics.AppendLine(
+ BuildUpdateDiagnostic(
+ worksheet.Name,
+ item,
+ updatedRows));
+ }
+
+ workbook.Save();
+
+ return new WriterDiagnosticResult
+ {
+ Success = true,
+ Message =
+ "XLSX update OK. Rows: " + totalUpdatedRows,
+ ExecutedTemplate =
+ diagnostics.ToString().TrimEnd()
+ };
+ }
+ }
+
+ ///
+ /// Validates the configured data source path and file extension.
+ ///
+ ///
+ /// A successful result when the data source is valid; otherwise,
+ /// a failed diagnostic result.
+ ///
+ private WriterDiagnosticResult ValidateDataSource()
+ {
+ if (string.IsNullOrWhiteSpace(cfg.DataSource))
+ return Fail("XLSX file path is not defined.");
+
+ string extension = Path.GetExtension(cfg.DataSource);
+
+ if (!string.Equals(
+ extension,
+ ".xlsx",
+ StringComparison.OrdinalIgnoreCase))
+ {
+ return Fail(
+ "Invalid file extension. Expected .xlsx.");
+ }
+
+ return Ok("XLSX data source is valid.");
+ }
+
+ ///
+ /// Opens the configured workbook.
+ ///
+ ///
+ /// An opened .
+ ///
+ private XLWorkbook OpenWorkbook()
+ {
+ return new XLWorkbook(cfg.DataSource);
+ }
+
+ ///
+ /// Creates an empty workbook containing the default worksheet.
+ ///
+ ///
+ /// Destination path of the workbook.
+ ///
+ private void CreateEmptyWorkbook(string path)
+ {
+ using (XLWorkbook workbook = new XLWorkbook())
+ {
+ workbook.Worksheets.Add(DefaultWorksheetName);
+ workbook.SaveAs(path);
+ }
+ }
+
+ ///
+ /// Gets the first worksheet from the specified workbook.
+ ///
+ ///
+ /// Workbook containing the worksheet.
+ ///
+ ///
+ /// The first worksheet in the workbook.
+ ///
+ ///
+ /// Thrown when the workbook does not contain a worksheet.
+ ///
+ private IXLWorksheet GetFirstWorksheet(XLWorkbook workbook)
+ {
+ if (workbook == null)
+ throw new ArgumentNullException(nameof(workbook));
+
+ IXLWorksheet worksheet =
+ workbook.Worksheets.FirstOrDefault();
+
+ if (worksheet == null)
+ {
+ throw new InvalidOperationException(
+ "XLSX workbook does not contain any worksheet.");
+ }
+
+ return worksheet;
+ }
+
+ ///
+ /// Reads header names from the first worksheet row.
+ ///
+ ///
+ /// Worksheet containing the header row.
+ ///
+ ///
+ /// A case-insensitive dictionary mapping column names to their
+ /// one-based worksheet column numbers.
+ ///
+ private Dictionary GetHeaderColumns(
+ IXLWorksheet worksheet)
+ {
+ Dictionary columns =
+ new Dictionary(
+ StringComparer.OrdinalIgnoreCase);
+
+ IXLRow headerRow = worksheet.Row(1);
+ IXLCell lastUsedCell = headerRow.LastCellUsed();
+
+ if (lastUsedCell == null)
+ return columns;
+
+ int lastColumnNumber = lastUsedCell.Address.ColumnNumber;
+
+ for (int columnNumber = 1;
+ columnNumber <= lastColumnNumber;
+ columnNumber++)
+ {
+ string columnName = headerRow
+ .Cell(columnNumber)
+ .GetFormattedString()
+ .Trim();
+
+ if (string.IsNullOrWhiteSpace(columnName))
+ continue;
+
+ if (columns.ContainsKey(columnName))
+ {
+ throw new InvalidOperationException(
+ "XLSX header contains duplicate column: " +
+ columnName);
+ }
+
+ columns.Add(columnName, columnNumber);
+ }
+
+ return columns;
+ }
+
+ ///
+ /// Reads existing worksheet headers or creates them when the worksheet
+ /// is empty.
+ ///
+ ///
+ /// Worksheet containing the header row.
+ ///
+ ///
+ /// Column names required by the insert operation.
+ ///
+ ///
+ /// A dictionary mapping column names to worksheet column numbers.
+ ///
+ private Dictionary GetOrCreateHeaderColumns(
+ IXLWorksheet worksheet,
+ IEnumerable requiredColumnNames)
+ {
+ Dictionary columns =
+ GetHeaderColumns(worksheet);
+
+ if (columns.Count > 0)
+ return columns;
+
+ string[] columnNames = requiredColumnNames
+ .Where(name => !string.IsNullOrWhiteSpace(name))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+
+ for (int index = 0; index < columnNames.Length; index++)
+ {
+ int columnNumber = index + 1;
+
+ worksheet.Cell(1, columnNumber).Value =
+ columnNames[index];
+
+ columns.Add(
+ columnNames[index],
+ columnNumber);
+ }
+
+ return columns;
+ }
+
+ ///
+ /// Determines the next available data row number.
+ ///
+ ///
+ /// Worksheet into which data will be inserted.
+ ///
+ ///
+ /// The one-based row number following the last used row.
+ ///
+ private int GetNextDataRowNumber(IXLWorksheet worksheet)
+ {
+ IXLRow lastUsedRow = worksheet.LastRowUsed();
+
+ if (lastUsedRow == null)
+ return 2;
+
+ return Math.Max(lastUsedRow.RowNumber() + 1, 2);
+ }
+
+ ///
+ /// Validates a single update item.
+ ///
+ ///
+ /// Update item to validate.
+ ///
+ ///
+ /// Available worksheet columns.
+ ///
+ ///
+ /// A successful result when the update item is valid; otherwise,
+ /// a failed diagnostic result.
+ ///
+ private WriterDiagnosticResult ValidateUpdateItem(
+ UpdateWriteItem item,
+ IDictionary columns)
+ {
+ if (string.IsNullOrWhiteSpace(item.WhereParameterName))
+ return Fail("Update WHERE column name is empty.");
+
+ if (string.IsNullOrWhiteSpace(item.SetParameterName))
+ return Fail("Update SET column name is empty.");
+
+ if (!columns.ContainsKey(item.WhereParameterName))
+ {
+ return Fail(
+ "XLSX worksheet does not contain WHERE column: " +
+ item.WhereParameterName);
+ }
+
+ if (!columns.ContainsKey(item.SetParameterName))
+ {
+ return Fail(
+ "XLSX worksheet does not contain SET column: " +
+ item.SetParameterName);
+ }
+
+ return Ok("Update item is valid.");
+ }
+
+ ///
+ /// Updates all worksheet rows matching the specified value.
+ ///
+ ///
+ /// Worksheet containing the data.
+ ///
+ ///
+ /// One-based column number used to locate matching rows.
+ ///
+ ///
+ /// Value that must match the current cell value.
+ ///
+ ///
+ /// One-based column number of the cell to update.
+ ///
+ ///
+ /// New value assigned to the target cell.
+ ///
+ ///
+ /// The number of updated rows.
+ ///
+ private int UpdateMatchingRows(
+ IXLWorksheet worksheet,
+ int whereColumnNumber,
+ string whereValue,
+ int setColumnNumber,
+ string setValue)
+ {
+ IXLRow lastUsedRow = worksheet.LastRowUsed();
+
+ if (lastUsedRow == null ||
+ lastUsedRow.RowNumber() < 2)
+ {
+ return 0;
+ }
+
+ int updatedRows = 0;
+ int lastRowNumber = lastUsedRow.RowNumber();
+
+ for (int rowNumber = 2;
+ rowNumber <= lastRowNumber;
+ rowNumber++)
+ {
+ IXLCell whereCell =
+ worksheet.Cell(rowNumber, whereColumnNumber);
+
+ string currentValue =
+ GetCellComparisonValue(whereCell);
+
+ if (!string.Equals(
+ currentValue,
+ whereValue ?? string.Empty,
+ StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ IXLCell setCell =
+ worksheet.Cell(rowNumber, setColumnNumber);
+
+ SetCellValue(setCell, setValue);
+ updatedRows++;
+ }
+
+ return updatedRows;
+ }
+
+ ///
+ /// Converts an XLSX cell value to a string used for update comparison.
+ ///
+ ///
+ /// Cell whose value is converted.
+ ///
+ ///
+ /// The formatted cell value, or an empty string for an empty cell.
+ ///
+ private string GetCellComparisonValue(IXLCell cell)
+ {
+ if (cell == null || cell.IsEmpty())
+ return string.Empty;
+
+ return cell.GetFormattedString();
+ }
+
+ ///
+ /// Assigns a request value to an XLSX cell.
+ ///
+ ///
+ /// Target worksheet cell.
+ ///
+ ///
+ /// Value to assign. A value clears the cell.
+ ///
+ private void SetCellValue(IXLCell cell, string value)
+ {
+ if (value == null)
+ {
+ cell.Clear(XLClearOptions.Contents);
+ return;
+ }
+
+ cell.Value = value;
+ }
+
+ ///
+ /// Builds a diagnostic description of an insert operation.
+ ///
+ ///
+ /// Name of the target worksheet.
+ ///
+ ///
+ /// Row number written by the operation.
+ ///
+ ///
+ /// Inserted values.
+ ///
+ ///
+ /// A human-readable insert diagnostic string.
+ ///
+ private string BuildInsertDiagnostic(
+ string worksheetName,
+ int rowNumber,
+ IEnumerable items)
+ {
+ string values = string.Join(
+ "; ",
+ items.Select(
+ item =>
+ item.ColumnName +
+ "=" +
+ ToDiagnosticValue(item.Value)));
+
+ return string.Format(
+ "INSERT [{0}] ROW {1}: {2}",
+ worksheetName,
+ rowNumber,
+ values);
+ }
+
+ ///
+ /// Builds a diagnostic description of an update operation.
+ ///
+ ///
+ /// Name of the target worksheet.
+ ///
+ ///
+ /// Executed update item.
+ ///
+ ///
+ /// Number of rows updated by the operation.
+ ///
+ ///
+ /// A human-readable update diagnostic string.
+ ///
+ private string BuildUpdateDiagnostic(
+ string worksheetName,
+ UpdateWriteItem item,
+ int updatedRows)
+ {
+ return string.Format(
+ "UPDATE [{0}] SET {1}={2} WHERE {3}={4}; Rows={5}",
+ worksheetName,
+ item.SetParameterName,
+ ToDiagnosticValue(item.SetValue),
+ item.WhereParameterName,
+ ToDiagnosticValue(item.WhereValue),
+ updatedRows);
+ }
+
+ ///
+ /// Converts a value to a diagnostic representation.
+ ///
+ ///
+ /// Value to represent.
+ ///
+ ///
+ /// The escaped diagnostic value.
+ ///
+ private string ToDiagnosticValue(string value)
+ {
+ if (value == null)
+ return "NULL";
+
+ return "'" + value.Replace("'", "''") + "'";
+ }
+
+ ///
+ /// Creates a successful diagnostic result.
+ ///
+ ///
+ /// Diagnostic message.
+ ///
+ ///
+ /// A successful writer diagnostic result.
+ ///
+ private WriterDiagnosticResult Ok(string message)
+ {
+ return new WriterDiagnosticResult
+ {
+ Success = true,
+ Message = message
+ };
+ }
+
+ ///
+ /// Creates a failed diagnostic result.
+ ///
+ ///
+ /// Diagnostic error message.
+ ///
+ ///
+ /// A failed writer diagnostic result.
+ ///
+ private WriterDiagnosticResult Fail(string message)
+ {
+ return new WriterDiagnosticResult
+ {
+ Success = false,
+ Message = message
+ };
+ }
+
+ ///
+ /// Resolves the worksheet name from the configured XLSX write template.
+ ///
+ ///
+ /// Template containing either a worksheet definition in the format
+ /// Sheet=WorksheetName or an SQL-like insert/update command.
+ ///
+ ///
+ /// The worksheet name resolved from the template.
+ ///
+ ///
+ /// Thrown when the template is empty or the worksheet name cannot be resolved.
+ ///
+ private string ParseWorksheetName(string template)
+ {
+ if (string.IsNullOrWhiteSpace(template))
+ throw new InvalidOperationException(
+ "XLSX write template is empty.");
+
+ string value = template.Trim();
+
+ const string sheetPrefix = "Sheet=";
+
+ if (value.StartsWith(
+ sheetPrefix,
+ StringComparison.OrdinalIgnoreCase))
+ {
+ return ValidateAndNormalizeWorksheetName(
+ value.Substring(sheetPrefix.Length));
+ }
+
+ const string insertPrefix = "INSERT INTO ";
+
+ if (value.StartsWith(
+ insertPrefix,
+ StringComparison.OrdinalIgnoreCase))
+ {
+ string remaining = value
+ .Substring(insertPrefix.Length)
+ .Trim();
+
+ int endIndex = remaining.IndexOfAny(
+ new[] { ' ', '(' });
+
+ string worksheetName = endIndex >= 0
+ ? remaining.Substring(0, endIndex)
+ : remaining;
+
+ return ValidateAndNormalizeWorksheetName(
+ worksheetName);
+ }
+
+ const string updatePrefix = "UPDATE ";
+
+ if (value.StartsWith(
+ updatePrefix,
+ StringComparison.OrdinalIgnoreCase))
+ {
+ string remaining = value
+ .Substring(updatePrefix.Length)
+ .Trim();
+
+ int endIndex = remaining.IndexOfAny(
+ new[] { ' ', '(' });
+
+ string worksheetName = endIndex >= 0
+ ? remaining.Substring(0, endIndex)
+ : remaining;
+
+ return ValidateAndNormalizeWorksheetName(
+ worksheetName);
+ }
+
+ throw new InvalidOperationException(
+ "XLSX worksheet name could not be resolved from template: " +
+ template);
+ }
+
+ ///
+ /// Normalizes and validates an XLSX worksheet name.
+ ///
+ ///
+ /// Worksheet name to normalize and validate.
+ ///
+ ///
+ /// A valid worksheet name.
+ ///
+ ///
+ /// Thrown when the worksheet name is empty, too long,
+ /// or contains an invalid character.
+ ///
+ private string ValidateAndNormalizeWorksheetName(
+ string worksheetName)
+ {
+ string result = (worksheetName ?? string.Empty).Trim();
+
+ if (result.Length >= 2)
+ {
+ bool squareBrackets =
+ result[0] == '[' &&
+ result[result.Length - 1] == ']';
+
+ bool singleQuotes =
+ result[0] == '\'' &&
+ result[result.Length - 1] == '\'';
+
+ bool doubleQuotes =
+ result[0] == '"' &&
+ result[result.Length - 1] == '"';
+
+ if (squareBrackets || singleQuotes || doubleQuotes)
+ {
+ result = result.Substring(
+ 1,
+ result.Length - 2);
+ }
+ }
+
+ result = result.Trim();
+
+ if (string.IsNullOrWhiteSpace(result))
+ {
+ throw new InvalidOperationException(
+ "XLSX worksheet name is empty.");
+ }
+
+ if (result.Length > 31)
+ {
+ throw new InvalidOperationException(
+ "XLSX worksheet name cannot contain more than 31 characters.");
+ }
+
+ char[] invalidCharacters =
+ {
+ '\\',
+ '/',
+ '?',
+ '*',
+ '[',
+ ']',
+ ':'
+ };
+
+ if (result.IndexOfAny(invalidCharacters) >= 0)
+ {
+ throw new InvalidOperationException(
+ "XLSX worksheet name contains an invalid character: " +
+ result);
+ }
+
+ return result;
+ }
+
+ ///
+ /// Gets an existing worksheet or creates a new worksheet with the specified name.
+ ///
+ ///
+ /// Workbook containing the worksheet.
+ ///
+ ///
+ /// Name of the worksheet.
+ ///
+ ///
+ /// The existing or newly created worksheet.
+ ///
+ private IXLWorksheet GetOrCreateWorksheet(
+ XLWorkbook workbook,
+ string worksheetName)
+ {
+ if (workbook == null)
+ throw new ArgumentNullException(nameof(workbook));
+
+ IXLWorksheet worksheet;
+
+ if (workbook.Worksheets.TryGetWorksheet(
+ worksheetName,
+ out worksheet))
+ {
+ return worksheet;
+ }
+
+ return workbook.Worksheets.Add(worksheetName);
+ }
+ }
+}
diff --git a/TBF/TBF.csproj b/TBF/TBF.csproj
index a6e3bcf01..ea0d7da2f 100644
--- a/TBF/TBF.csproj
+++ b/TBF/TBF.csproj
@@ -108,6 +108,21 @@
..\packages\Castle.Core.5.1.1\lib\net462\Castle.Core.dll
+
+ ..\packages\ClosedXML.0.105.0\lib\netstandard2.0\ClosedXML.dll
+
+
+ ..\packages\ClosedXML.Parser.2.0.0\lib\netstandard2.0\ClosedXML.Parser.dll
+
+
+ ..\packages\DocumentFormat.OpenXml.3.1.1\lib\net46\DocumentFormat.OpenXml.dll
+
+
+ ..\packages\DocumentFormat.OpenXml.Framework.3.1.1\lib\net46\DocumentFormat.OpenXml.Framework.dll
+
+
+ ..\packages\ExcelNumberFormat.1.1.0\lib\net20\ExcelNumberFormat.dll
+
..\packages\FluentNHibernate.2.0.3.0\lib\net40\FluentNHibernate.dll
@@ -120,6 +135,9 @@
..\packages\Common\Logic.ProductionToProductMapper.dll
+
+ ..\packages\Microsoft.Bcl.HashCode.1.1.1\lib\net461\Microsoft.Bcl.HashCode.dll
+
..\packages\MySql.Data.6.6.5\lib\net40\MySql.Data.dll
@@ -141,10 +159,19 @@
..\packages\Oracle.ManagedDataAccess.19.11.0\lib\net40\Oracle.ManagedDataAccess.dll
True
+
+ ..\packages\RBush.Signed.4.0.0\lib\net47\RBush.dll
+
..\packages\Renci.SshNet\Renci.SshNet.dll
+
+ ..\packages\SixLabors.Fonts.1.0.0\lib\netstandard2.0\SixLabors.Fonts.dll
+
+
+ ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll
+
@@ -157,13 +184,19 @@
..\packages\System.Drawing.Common.9.0.5\lib\net462\System.Drawing.Common.dll
+
+ ..\packages\System.Memory.4.5.5\lib\net461\System.Memory.dll
+
..\packages\System.Net.Sockets.4.3.0\lib\net46\System.Net.Sockets.dll
-
- ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.3\lib\net461\System.Runtime.CompilerServices.Unsafe.dll
+
+ ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll
+
+
+ ..\packages\System.Runtime.CompilerServices.Unsafe.4.7.0\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll
@@ -173,6 +206,7 @@
+
..\packages\Common\Xylem.Common.CommonCore.dll
@@ -1223,6 +1257,7 @@
WriterCfgCtrl.cs
+
UserControl
diff --git a/TBF/app.config b/TBF/app.config
index 64516adb1..a5310bf9b 100644
--- a/TBF/app.config
+++ b/TBF/app.config
@@ -83,6 +83,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/TBF/packages.config b/TBF/packages.config
index 609408d25..478c4224c 100644
--- a/TBF/packages.config
+++ b/TBF/packages.config
@@ -1,18 +1,29 @@
+
+
+
+
+
+
+
+
+
+
-
+
+
\ No newline at end of file
diff --git a/TBFTests/app.config b/TBFTests/app.config
index 79f5b8a14..84e18af51 100644
--- a/TBFTests/app.config
+++ b/TBFTests/app.config
@@ -14,6 +14,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ToFirstMonitor/App.config b/ToFirstMonitor/App.config
index dd580728f..8d668f958 100644
--- a/ToFirstMonitor/App.config
+++ b/ToFirstMonitor/App.config
@@ -9,6 +9,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ToSecondMonitor/App.config b/ToSecondMonitor/App.config
index dd580728f..8d668f958 100644
--- a/ToSecondMonitor/App.config
+++ b/ToSecondMonitor/App.config
@@ -9,6 +9,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+