diff --git a/CSharpBible/Data/Data.sln b/CSharpBible/Data/Data.sln index a5e22fdd2..2fd30a711 100644 --- a/CSharpBible/Data/Data.sln +++ b/CSharpBible/Data/Data.sln @@ -3,8 +3,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 VisualStudioVersion = 18.0.11111.16 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RepoMigrator.Core", "RepoMigrator\RepoMigrator.Core\RepoMigrator.Core\RepoMigrator.Core.csproj", "{5281BA89-6796-4C3E-8DDA-7A627896AC1A}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RepoMigrator", "RepoMigrator", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" ProjectSection(SolutionItems) = preProject Directory.Build.props = Directory.Build.props @@ -72,6 +70,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PictureDB.UI", "PictureDB\P EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommonDialogs_net", "..\Libraries\CommonDialogs\CommonDialogs_net.csproj", "{65F08D9B-F63E-14C2-C35D-C324E1E37785}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RepoMigrator.Core", "RepoMigrator\RepoMigrator.Core\RepoMigrator.Core.csproj", "{7C507273-B03C-6545-08E2-F89BFF1DEDAA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -82,18 +82,6 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {5281BA89-6796-4C3E-8DDA-7A627896AC1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5281BA89-6796-4C3E-8DDA-7A627896AC1A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5281BA89-6796-4C3E-8DDA-7A627896AC1A}.Debug|x64.ActiveCfg = Debug|Any CPU - {5281BA89-6796-4C3E-8DDA-7A627896AC1A}.Debug|x64.Build.0 = Debug|Any CPU - {5281BA89-6796-4C3E-8DDA-7A627896AC1A}.Debug|x86.ActiveCfg = Debug|Any CPU - {5281BA89-6796-4C3E-8DDA-7A627896AC1A}.Debug|x86.Build.0 = Debug|Any CPU - {5281BA89-6796-4C3E-8DDA-7A627896AC1A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5281BA89-6796-4C3E-8DDA-7A627896AC1A}.Release|Any CPU.Build.0 = Release|Any CPU - {5281BA89-6796-4C3E-8DDA-7A627896AC1A}.Release|x64.ActiveCfg = Release|Any CPU - {5281BA89-6796-4C3E-8DDA-7A627896AC1A}.Release|x64.Build.0 = Release|Any CPU - {5281BA89-6796-4C3E-8DDA-7A627896AC1A}.Release|x86.ActiveCfg = Release|Any CPU - {5281BA89-6796-4C3E-8DDA-7A627896AC1A}.Release|x86.Build.0 = Release|Any CPU {387BDBC9-0123-4C86-98FA-FBF66522A4B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {387BDBC9-0123-4C86-98FA-FBF66522A4B9}.Debug|Any CPU.Build.0 = Debug|Any CPU {387BDBC9-0123-4C86-98FA-FBF66522A4B9}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -382,12 +370,23 @@ Global {65F08D9B-F63E-14C2-C35D-C324E1E37785}.Release|x64.Build.0 = Release|Any CPU {65F08D9B-F63E-14C2-C35D-C324E1E37785}.Release|x86.ActiveCfg = Release|x86 {65F08D9B-F63E-14C2-C35D-C324E1E37785}.Release|x86.Build.0 = Release|x86 + {7C507273-B03C-6545-08E2-F89BFF1DEDAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C507273-B03C-6545-08E2-F89BFF1DEDAA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C507273-B03C-6545-08E2-F89BFF1DEDAA}.Debug|x64.ActiveCfg = Debug|Any CPU + {7C507273-B03C-6545-08E2-F89BFF1DEDAA}.Debug|x64.Build.0 = Debug|Any CPU + {7C507273-B03C-6545-08E2-F89BFF1DEDAA}.Debug|x86.ActiveCfg = Debug|Any CPU + {7C507273-B03C-6545-08E2-F89BFF1DEDAA}.Debug|x86.Build.0 = Debug|Any CPU + {7C507273-B03C-6545-08E2-F89BFF1DEDAA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C507273-B03C-6545-08E2-F89BFF1DEDAA}.Release|Any CPU.Build.0 = Release|Any CPU + {7C507273-B03C-6545-08E2-F89BFF1DEDAA}.Release|x64.ActiveCfg = Release|Any CPU + {7C507273-B03C-6545-08E2-F89BFF1DEDAA}.Release|x64.Build.0 = Release|Any CPU + {7C507273-B03C-6545-08E2-F89BFF1DEDAA}.Release|x86.ActiveCfg = Release|Any CPU + {7C507273-B03C-6545-08E2-F89BFF1DEDAA}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {5281BA89-6796-4C3E-8DDA-7A627896AC1A} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {387BDBC9-0123-4C86-98FA-FBF66522A4B9} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {0256D739-F882-4F8C-8820-570FB07687AA} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {DB10ACCB-609B-4638-8629-89196580CB43} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} @@ -410,6 +409,7 @@ Global {42156DBB-5FC0-3FE1-FC43-55400E7FDFAD} = {3C73C616-12F2-478C-9CAC-823780861BCD} {BFCB4F4A-F6A3-EB13-DB02-B0C1979AFDEA} = {3C73C616-12F2-478C-9CAC-823780861BCD} {65F08D9B-F63E-14C2-C35D-C324E1E37785} = {51F6C20B-003C-430C-BED7-2A89F834E4C0} + {7C507273-B03C-6545-08E2-F89BFF1DEDAA} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {128BE64A-28F5-47C5-A045-2352EF09BFBB} diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core.Tests/DataAnalysis.Core.Tests.csproj b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core.Tests/DataAnalysis.Core.Tests.csproj new file mode 100644 index 000000000..ddd4f0052 --- /dev/null +++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core.Tests/DataAnalysis.Core.Tests.csproj @@ -0,0 +1,24 @@ + + + net9.0;net8.0 + false + 12.0 + enable + enable + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core.Tests/Export/TableExcelExporterTests.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core.Tests/Export/TableExcelExporterTests.cs new file mode 100644 index 000000000..42496f870 --- /dev/null +++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core.Tests/Export/TableExcelExporterTests.cs @@ -0,0 +1,157 @@ +using System; +using System.Reflection; +using ClosedXML.Excel; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NSubstitute; +using DataAnalysis.Core.Export; + +namespace DataAnalysis.Core.Tests; + +// Pseudocode (Plan): +// 1. Per Reflection die private Methode ToXLCellValue ermitteln: BindingFlags.Instance | BindingFlags.NonPublic. +// 2. Instanz von TableExcelExporter erzeugen. +// 3. DataTestMethod mit mehreren DataRow Fällen: +// - null -> string.Empty +// - DBNull.Value -> string.Empty +// - bool true/false -> 1/0 (int) +// - int -> int +// - long im int-Bereich -> int +// - long außerhalb int-Bereich -> double +// - float/double Ganzzahl-Wert -> int +// - float/double mit Nachkommastellen -> double +// - double.NaN / double.PositiveInfinity -> kulturinvariant string ("NaN","Infinity") +// - string leer/Whitespace -> string.Empty +// - string Integer -> int +// - string Double -> double +// 4. Methode via MethodInfo.Invoke aufrufen, Ergebnis (XLCellValue) typ und Wert prüfen. +// 5. Separater Test: Interface ITableExporter via NSubstitute erzeugen und verifizieren, +// dass Reflection auf Interface die Methode nicht findet, aber auf konkretem Typ schon. +// 6. Assertions kurz und präzise. +// 7. Nutzung NSubstitute sicherstellen (Substitute.For()). + +[TestClass] +public class TableExcelExporterTests +{ + private static MethodInfo GetPrivateMethod(string name) + => typeof(TableExcelExporter).GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException($"Methode {name} nicht gefunden."); + + [DataTestMethod] + [DataRow(TypeCode.Empty, null, "", TypeCode.String)] // null -> "" + [DataRow(TypeCode.DBNull, null, "", TypeCode.String)] // DBNull -> "" + [DataRow(TypeCode.Boolean, true, true, TypeCode.Boolean)] // true -> 1 + [DataRow(TypeCode.Boolean, false, false, TypeCode.Boolean)] // false -> 0 + [DataRow(TypeCode.Int32, 5, 5, TypeCode.Int32)] // int bleibt int + [DataRow(TypeCode.Int64, 5L, 5, TypeCode.Int32)] // long im int-Bereich -> int + [DataRow(TypeCode.Int64, 5000000000L, 5000000000d, TypeCode.Double)] // long außerhalb int-Bereich -> double + [DataRow(TypeCode.Single, 5.0f, 5, TypeCode.Int32)] // float Ganzzahl -> int + [DataRow(TypeCode.Single, 5.25f, 5.25d, TypeCode.Double)] // float mit Nachkommastellen -> double + [DataRow(TypeCode.Double, 5.0d, 5, TypeCode.Int32)] // double Ganzzahl -> int + [DataRow(TypeCode.Double, 5.125d, 5.125d, TypeCode.Double)] // double mit Nachkommastellen -> double + [DataRow(TypeCode.Double, double.NaN, "NaN", TypeCode.String)] // NaN -> string "NaN" + [DataRow(TypeCode.Double, double.PositiveInfinity, "Infinity", TypeCode.String)] // Infinity -> string + [DataRow(TypeCode.String, " ", "", TypeCode.String)] // Whitespace -> "" + [DataRow(TypeCode.String, "True", true, TypeCode.Boolean)] // String int -> int + [DataRow(TypeCode.String, "false", false, TypeCode.Boolean)] // String int -> int + [DataRow(TypeCode.String, "42", 42, TypeCode.Int32)] // String int -> int + [DataRow(TypeCode.String, "42.75", 42.75d, TypeCode.Double)] // String double (.) -> double + [DataRow(TypeCode.String, "1e-6", 1e-6d, TypeCode.Double)] // String double (.) -> double + [DataRow(TypeCode.String, "-4.2e3", -4.2e3d, TypeCode.Double)] // String double (.) -> double + [DataRow(TypeCode.String, "42,75", 42.75d, TypeCode.Double)] // String double (,) -> double + [DataRow(TypeCode.String, "42.7.5", "42.7.5", TypeCode.String)] // Ungültig -> string unverändert + [DataRow(TypeCode.String, "0010", "0010", TypeCode.String)] // Führende Nullen -> string + [DataRow(TypeCode.String, "0", 0, TypeCode.Int32)] // nur Null -> int + public void ToXLCellValue_Test(TypeCode inputTypeCode, object? inputRaw, object expectedValue, TypeCode expectedTypeCode) + { + // Eingabeobjekt aus TypeCode ableiten + object? input = inputTypeCode switch + { + TypeCode.Empty => null, + TypeCode.DBNull => DBNull.Value, + _ => inputRaw + }; + + // Erwarteten Typ aus TypeCode bestimmen + Type expectedType = expectedTypeCode switch + { + TypeCode.String => typeof(string), + TypeCode.Int32 => typeof(int), + TypeCode.Boolean => typeof(bool), + TypeCode.Double => typeof(double), + _ => throw new AssertFailedException("Nicht unterstützter erwarteter TypeCode.") + }; + + var exporter = new TableExcelExporter(); + var mi = GetPrivateMethod("ToXLCellValue"); + + var xl = (XLCellValue)mi.Invoke(exporter, new[] { input })!; + + object actualValue; + TypeCode actualTC; + if (xl.IsBlank) + { + actualValue = ""; + actualTC = TypeCode.String; + } + else if (xl.IsText) + { + actualValue = xl.GetText(); + actualTC = TypeCode.String; + } + else if (xl.IsNumber) + { + actualValue = xl.GetNumber(); + actualTC = Math.Abs((double)actualValue % 1d) < 1e-10 ? TypeCode.Int32 : TypeCode.Double; + } + else if (xl.IsBoolean) + { + actualValue = xl.GetBoolean(); + actualTC = TypeCode.Boolean; + } + else + { + actualValue = xl.ToString(); + actualTC = TypeCode.String; + } + + if (expectedType == typeof(string)) + { + Assert.AreEqual(expectedValue.ToString(), actualValue?.ToString(), "Stringwert stimmt nicht."); + return; + } + + if (expectedType == typeof(int)) + { + Assert.AreEqual(expectedTypeCode, actualTC , "Erwartet int."); + Assert.AreEqual((int)expectedValue, (int)Math.Round((double)actualValue), "int-Wert stimmt nicht."); + } + else if (expectedType == typeof(double)) + { + Assert.IsTrue(actualValue is double or int, "Erwartet double oder int Repräsentation."); + double actualDouble = actualValue is int ai ? ai : (double)actualValue; + Assert.AreEqual(Convert.ToDouble(expectedValue), actualDouble, 1e-12, "double-Wert stimmt nicht."); + } + else if (expectedType == typeof(bool)) + { + Assert.IsInstanceOfType(actualValue, typeof(bool), "Erwartet bool."); + Assert.AreEqual((bool)expectedValue, (bool)actualValue, "bool-Wert stimmt nicht."); + } + else + { + Assert.Fail("Nicht unterstützter erwarteter Typ."); + } + } + + [TestMethod] + public void Reflection_Finden_Der_Privaten_Methode_Ueber_Konkreten_Typ_Nicht_Ueber_Interface() + { + // Arrange + var ifaceSub = Substitute.For(); + var viaInterface = ifaceSub.GetType().GetMethod("ToXLCellValue", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + var viaConcrete = typeof(TableExcelExporter).GetMethod("ToXLCellValue", BindingFlags.Instance | BindingFlags.NonPublic); + + // Assert + Assert.IsNull(viaInterface, "Private Methode sollte über Interface-Proxy nicht gefunden werden."); + Assert.IsNotNull(viaConcrete, "Private Methode sollte über konkreten Typ gefunden werden."); + } +} \ No newline at end of file diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Export/Interfaces/ITableExporter.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Export/Interfaces/ITableExporter.cs index 12f30de78..b8039cac0 100644 --- a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Export/Interfaces/ITableExporter.cs +++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Export/Interfaces/ITableExporter.cs @@ -6,5 +6,5 @@ namespace DataAnalysis.Core.Export.Interfaces; public interface ITableExporter { - Task ExportAsync(DataTable table, string inputPath, string? outputPath, CancellationToken cancellationToken = default); + Task ExportAsync(DataTable table, string inputPath, string? outputPath, CancellationToken cancellationToken = default, Action? progressCallback = null); } diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Export/TableExcelExporter.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Export/TableExcelExporter.cs index 54f405517..43585f0b3 100644 --- a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Export/TableExcelExporter.cs +++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Export/TableExcelExporter.cs @@ -6,12 +6,13 @@ using System.Threading.Tasks; using ClosedXML.Excel; using DataAnalysis.Core.Export.Interfaces; +using DocumentFormat.OpenXml.Drawing.Diagrams; namespace DataAnalysis.Core.Export; public sealed class TableExcelExporter : ITableExporter { - public Task ExportAsync(DataTable table, string inputPath, string? outputPath, CancellationToken cancellationToken = default) + public Task ExportAsync(DataTable table, string inputPath, string? outputPath, CancellationToken cancellationToken = default, Action? progressCallback = null) { outputPath ??= BuildDefaultOutputPath(inputPath); Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!); @@ -19,32 +20,147 @@ public Task ExportAsync(DataTable table, string inputPath, string? outpu using var wb = new XLWorkbook(); var ws = wb.AddWorksheet(string.IsNullOrWhiteSpace(table.TableName) ? "Tabelle" : TrimSheetName(table.TableName)); + int totalRows = table.Rows.Count; + int processedRows = 0; + DateTime lastReport = DateTime.UtcNow; + void Report() + { + if (progressCallback is null) + return; + if ((DateTime.UtcNow - lastReport).TotalSeconds >= 1) + lock (progressCallback) + { + progressCallback(Math.Clamp(totalRows == 0 ? 1.0 : (double)processedRows / totalRows*0.7d, 0d, 0.7d)); + lastReport = DateTime.UtcNow; + } + } + // Header for (int c = 0; c < table.Columns.Count; c++) ws.Cell(1, c + 1).Value = table.Columns[c].ColumnName; ws.Range(1, 1, 1, Math.Max(1, table.Columns.Count)).Style.Font.Bold = true; // Rows - int r = 2; - foreach (DataRow row in table.Rows) + + Parallel.For(2, table.Rows.Count + 2, (r) => { + var row = table.Rows[r - 2]; cancellationToken.ThrowIfCancellationRequested(); for (int c = 0; c < table.Columns.Count; c++) if (table.Columns[c].DataType == typeof(DateTime) && row[c] is DateTime dt) { - ws.Cell(r, c + 1).Value = dt; - ws.Cell(r, c + 1).Style.DateFormat.Format = "yyyy-mm-dd hh:mm:ss.ms"; + lock (ws) + { + ws.Cell(r, c + 1).Value = dt; + ws.Cell(r, c + 1).Style.DateFormat.Format = "yyyy-mm-dd hh:mm:ss.ms"; + } } else - ws.Cell(r, c + 1).Value = row[c]?.ToString() ?? string.Empty; - r++; - } + { + var value = ToXLCellValue(row[c]); + // if (r==2) + // value = + lock (ws) + ws.Cell(r, c + 1).Value = value; + } + processedRows++; + if (r % 100 == 0) + Report(); + }); - ws.Columns().AdjustToContents(); + // ws.Columns().AdjustToContents(); + progressCallback?.Invoke(0.8); // Abschluss wb.SaveAs(outputPath); + progressCallback?.Invoke(1.0); // Abschluss return Task.FromResult(outputPath); } + private void UnParallel_For(int v1, int v2, Action value) + { + for (var i = v1; i < v2; i++) + value(i); + } + + private XLCellValue ToXLCellValue(object v) + { + static bool IsWholeNumber(double d) => Math.Abs(d % 1) < 1e-10; + + if (v is null || v == DBNull.Value) + return string.Empty; + + if (v is bool b) + return b ? true : false; + + switch (v) + { + case int i: + return i; + case long l: + if (l >= int.MinValue && l <= int.MaxValue) + return (int)l; + return (double)l; + case short s: + return (int)s; + case byte by: + return (int)by; + case sbyte sb: + return (int)sb; + case uint ui: + if (ui <= int.MaxValue) + return (int)ui; + return (double)ui; + case ulong ul: + if (ul <= (ulong)int.MaxValue) + return (int)ul; + return (double)ul; + case float f: + if (float.IsNaN(f) || float.IsInfinity(f)) + return f.ToString(System.Globalization.CultureInfo.InvariantCulture); + if (IsWholeNumber(f) && f >= int.MinValue && f <= int.MaxValue) + return (int)f; + return (double)f; + case double d: + if (double.IsNaN(d) || double.IsInfinity(d)) + return d.ToString(System.Globalization.CultureInfo.InvariantCulture); + if (IsWholeNumber(d) && d >= int.MinValue && d <= int.MaxValue) + return (int)d; + return d; + case string str: + { + if (str.ToLower() == "true") + return true; + if (str.ToLower() == "false") + return false; + + str = str.Trim(); + if (str.Length == 0) + return string.Empty; + + if (int.TryParse(str, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out var si)) + { + if (!str.StartsWith('0') || str == "0") + return si; + else + return str; + } + + if (double.TryParse(str, System.Globalization.NumberStyles.Float | System.Globalization.NumberStyles.AllowThousands, + System.Globalization.CultureInfo.InvariantCulture, out var sd)) + { + if (str.StartsWith('0')) + return str; + if (IsWholeNumber(sd) && sd >= int.MinValue && sd <= int.MaxValue) + return (int)sd; + return sd; + } + + return str; + } + } + + return v.ToString() ?? string.Empty; + } + private static string BuildDefaultOutputPath(string inputPath) { var dir = Path.GetDirectoryName(inputPath)!; diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Import/DelimitedTableReader.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Import/DelimitedTableReader.cs index fa3b3d76c..8dc36836a 100644 --- a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Import/DelimitedTableReader.cs +++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Import/DelimitedTableReader.cs @@ -8,12 +8,12 @@ using System.Globalization; using System.IO; using System.Linq; -using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; +using System.Threading.Channels; namespace DataAnalysis.Core.Import; @@ -30,7 +30,7 @@ public DelimitedTableReader(IDelimitedTableParsingProfile profile) _profile = profile ?? throw new ArgumentNullException(nameof(profile)); } - public async Task ReadTableAsync(string inputPath, CancellationToken cancellationToken = default) + public async Task ReadTableAsync(string inputPath, CancellationToken cancellationToken = default, Action? progressCallback = null) { if (string.IsNullOrWhiteSpace(inputPath)) throw new ArgumentException(ErrorPathEmpty, nameof(inputPath)); @@ -39,6 +39,8 @@ public async Task ReadTableAsync(string inputPath, CancellationToken var dt = new DataTable(_profile.TableName); using var fs = new FileStream(inputPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + long totalBytes = fs.Length; + long processedBytes = 0; using var reader = new StreamReader(fs, DetectEncoding(fs) ?? Encoding.UTF8, detectEncodingFromByteOrderMarks: true); // Header @@ -50,10 +52,11 @@ public async Task ReadTableAsync(string inputPath, CancellationToken var headerLine = await reader.ReadLineAsync().ConfigureAwait(false); if (headerLine is not null) { + processedBytes += Encoding.UTF8.GetByteCount(headerLine) + Environment.NewLine.Length; var headers = SplitLine(headerLine, _profile.Delimiter, _profile.Quote, _profile.TrimWhitespace); headerMap = headers.Select((h, i) => new { Name = (h ?? string.Empty).Trim(), i }) - .GroupBy(x => x.Name, StringComparer.OrdinalIgnoreCase) - .ToDictionary(g => g.Key, g => g.First().i, StringComparer.OrdinalIgnoreCase); + .GroupBy(x => x.Name, StringComparer.OrdinalIgnoreCase) + .ToDictionary(g => g.Key, g => g.First().i, StringComparer.OrdinalIgnoreCase); inclFields.AddRange(headerMap.Select((h, i) => i)); foreach (var h in headerMap) { @@ -62,7 +65,6 @@ public async Task ReadTableAsync(string inputPath, CancellationToken if (_profile.ExtractionRules.Any((r) => r.SourceColumn == h.Key)) inclFields.Remove(h.Value); } - } } @@ -71,50 +73,144 @@ public async Task ReadTableAsync(string inputPath, CancellationToken { EnsureColumn(dt, mapping.Target, mapping.IsDateTime); } - foreach (var i in inclFields) + if (headerMap is not null) + { + foreach (var i in inclFields) + { + var hmi = headerMap.Values.FirstOrDefault(v => v == i); + EnsureColumn(dt, headerMap.Keys.ToArray()[hmi]); + } + } + + DateTime lastReport = DateTime.UtcNow; + void ReportProgress() + { + if (progressCallback is null || totalBytes == 0) + return; + if ((DateTime.UtcNow - lastReport).TotalSeconds >= 1) + { + progressCallback(Math.Clamp((double)processedBytes / totalBytes, 0d, 1d)); + lastReport = DateTime.UtcNow; + } + } + ; + + var headerKeys = headerMap?.Keys.ToArray(); + + + /* + // PARALLELE VERARBEITUNG (Producer / Consumer) + var channel = Channel.CreateBounded(new BoundedChannelOptions(2048) + { + SingleWriter = true, + SingleReader = false, + FullMode = BoundedChannelFullMode.Wait + }); + + int workerCount = Math.Max(1, Environment.ProcessorCount); + + var consumers = Enumerable.Range(0, workerCount).Select(_ => Task.Run(async () => { - var hmi = headerMap.Values.FirstOrDefault((v) => (v == i)); - EnsureColumn(dt, headerMap.Keys.ToArray()[hmi]); + await foreach (var line in channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) + { + var flowControl = ProcessLine(dt, headerMap, inclFields, headerKeys, line); + if (!flowControl) + { + continue; + } + + } + }, cancellationToken)).ToArray(); + + // Producer liest Datei + string? readLine; + while ((readLine = await reader.ReadLineAsync().ConfigureAwait(false)) is not null) + { + cancellationToken.ThrowIfCancellationRequested(); + processedBytes += Encoding.UTF8.GetByteCount(readLine) + Environment.NewLine.Length; + ReportProgress(); + await channel.Writer.WriteAsync(readLine, cancellationToken).ConfigureAwait(false); } + channel.Writer.Complete(); - string? line; - while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) is not null) + await Task.WhenAll(consumers).ConfigureAwait(false); + */ + while (!reader.EndOfStream) { + var line = await reader.ReadLineAsync().ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); - if (string.IsNullOrWhiteSpace(line)) + processedBytes += Encoding.UTF8.GetByteCount(line) + Environment.NewLine.Length; + ReportProgress(); + var flowControl = ProcessLine(dt, headerMap, inclFields, headerKeys, line); + if (!flowControl) + { continue; - var fields = SplitLine(line, _profile.Delimiter, _profile.Quote, _profile.TrimWhitespace); + } + } + + progressCallback?.Invoke(1.0); + return dt; + } + + private bool ProcessLine(DataTable dt, Dictionary? headerMap, List inclFields, string[]? headerKeys, string? line) + { + if (string.IsNullOrWhiteSpace(line)) + return false; + + var fields = SplitLine(line, _profile.Delimiter, _profile.Quote, _profile.TrimWhitespace); - // extraction rules - var attributes = new Dictionary(StringComparer.OrdinalIgnoreCase); - ApplyExtractionRules(fields, headerMap, attributes); + // extraction rules + var attributes = new Dictionary(StringComparer.OrdinalIgnoreCase); + ApplyExtractionRules(fields, headerMap, attributes); - // grow columns for attributes if needed + // grow columns for attributes if needed + if (attributes.Count > 0) + { foreach (var key in attributes.Keys) { if (!_profile.FixedColumns.Any((f) => f.Source == key)) EnsureColumn(dt, key); } + } - var row = dt.NewRow(); + // DataRow füllen + var rowValues = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var mapping in _profile.FixedColumns) - row[mapping.Target] = Extract(mapping, fields, headerMap, attributes); + foreach (var mapping in _profile.FixedColumns) + { + var value = Extract(mapping, fields, headerMap, attributes); + rowValues[mapping.Target] = value ?? string.Empty; + } - foreach (var i in inclFields) + if (headerMap is not null && headerKeys is not null) + { + foreach (var iField in inclFields) { - var hmi = headerMap.Values.FirstOrDefault((v) => (v == i)); - if (fields.Count>i) - row[headerMap.Keys.ToArray()[hmi]] = fields[i]; + if (fields.Count > iField) + { + var hmi = headerMap.Values.FirstOrDefault(v => v == iField); + var colName = headerKeys[hmi]; + rowValues[colName] = fields[iField] ?? string.Empty; + } } + } - foreach (var kv in attributes) - if (!_profile.FixedColumns.Any((f) => f.Source == kv.Key)) - row[kv.Key] = kv.Value ?? string.Empty; - dt.Rows.Add(row); + foreach (var kv in attributes) + { + if (!_profile.FixedColumns.Any((f) => f.Source == kv.Key)) + rowValues[kv.Key] = kv.Value ?? string.Empty; } - return dt; + + lock (dt) + { + var row = dt.NewRow(); + foreach (var kv in rowValues) + row[kv.Key] = kv.Value ?? string.Empty; + + dt.Rows.Add(row); + } + return true; } private void ApplyExtractionRules(IReadOnlyList fields, Dictionary? headerMap, Dictionary attributes) @@ -152,7 +248,6 @@ private void ApplyExtractionRules(IReadOnlyList fields, Dictionary fields, Dictionary fields, - Dictionary? headerMap, - IReadOnlyDictionary? attributes = null) + FixedColumnMapping mapping, + IReadOnlyList fields, + Dictionary? headerMap, + IReadOnlyDictionary? attributes = null) { - // Hilfsfunktionen int? ResolveFieldIndex(string? selector) { if (string.IsNullOrWhiteSpace(selector)) @@ -199,9 +293,7 @@ private string Extract( return null; } - // Werte über fields (Index/Header) ODER attributes extrahieren - var tsRaw = GetBySelector(mapping.Source, out var _); - + var tsRaw = GetBySelector(mapping.Source, out _); return tsRaw; } @@ -233,9 +325,14 @@ private string Extract( if (quote.HasValue && c == q) { if (inQuotes && i + 1 < line.Length && line[i + 1] == q) - { sb.Append(q); i++; } + { + sb.Append(q); + i++; + } else - { inQuotes = !inQuotes; } + { + inQuotes = !inQuotes; + } continue; } if (!inQuotes && c == delimiter) diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Import/Interfaces/ITableReader.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Import/Interfaces/ITableReader.cs index 19ee41d88..9fdc29cb5 100644 --- a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Import/Interfaces/ITableReader.cs +++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Import/Interfaces/ITableReader.cs @@ -4,5 +4,5 @@ namespace DataAnalysis.Core.Import.Interfaces; public interface ITableReader { - Task ReadTableAsync(string inputPath, CancellationToken cancellationToken = default); + Task ReadTableAsync(string inputPath, CancellationToken cancellationToken = default, Action? progressCallback = null); } \ No newline at end of file diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Models/AnalysisAggregateProfile.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Models/AnalysisAggregateProfile.cs index 62d21f842..31ff305ec 100644 --- a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Models/AnalysisAggregateProfile.cs +++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Models/AnalysisAggregateProfile.cs @@ -24,7 +24,29 @@ public sealed class AnalysisAggregateProfile new AnalysisQuery { Title = "Top-Events2", Dimensions = new [] { DimensionKind.MessageNormalized }, TopN =30, Filter = new ValueFilterDefinition { Field = "Severity", Type = "Enum", Operator = FilterOperator.Le, Value = "Error" }, }, new AnalysisQuery { Title = "Severity x Source", Dimensions = new [] { DimensionKind.Source, DimensionKind.Severity }, Columns = Enum.GetValues().Select(s => s.ToString()).ToArray() }, new AnalysisQuery { Title = "Events x Source (Top50)", Dimensions = new [] { DimensionKind.Source, DimensionKind.MessageNormalized }, TopN =300}, - new AnalysisQuery { Title = "Koordinate Cluster", Dimensions = new [] { DimensionKind.X, DimensionKind.Y }, TopN = 30, IsDBScan = true, DbEps = 10.0, DbMinPts = 3, Filter = new ValueFilterDefinition { Field = "Severity", Type = "Enum", Operator = FilterOperator.Le, Value = "Error" }, } + new AnalysisQuery { Title = "Error Cluster", Dimensions = new [] { DimensionKind.X, DimensionKind.Y }, TopN = 30, IsDBScan = true, DbEps = 2.0, DbMinPts = 3, Filter = new ValueFilterDefinition { Field = "Severity", Type = "Enum", Operator = FilterOperator.Le, Value = "Error" }, }, + new AnalysisQuery { Title = "Max-G Cluster (1.0)", Dimensions = new [] { DimensionKind.X, DimensionKind.Y }, TopN = 30, IsDBScan = true, DbEps = 1.0, DbMinPts = 3, Filter = new GroupFilterDefinition { Mode="And", Type = "group", + Filters=[ new ValueFilterDefinition { Field = "Message", Type = "String", Operator = FilterOperator.Eq, Value = "Max. G-Force" }, + new ValueFilterDefinition { Field = "X", Type = "value", Operator = FilterOperator.Gt, Value = "10" } + ] }, }, + new AnalysisQuery { Title = "Max-G Cluster (0.5)", Dimensions = new [] { DimensionKind.X, DimensionKind.Y }, TopN = 50, IsDBScan = true, DbEps = 0.5, DbMinPts = 3, Filter = + new GroupFilterDefinition { Mode="And", Type = "group", + Filters=[ + new ValueFilterDefinition { Field = "Message", Type = "String", Operator = FilterOperator.Eq, Value = "Max. G-Force" }, + new ValueFilterDefinition { Field = "X", Type = "value", Operator = FilterOperator.Gt, Value = "10" } + ] }, }, + new AnalysisQuery { Title = "SSCU Cluster (0.5)", Dimensions = new [] { DimensionKind.X, DimensionKind.Y }, TopN = 30, IsDBScan = true, DbEps = 0.5, DbMinPts = 3, Filter = + new GroupFilterDefinition { Mode="And", Type = "group", Filters=[ + new ValueFilterDefinition { Field = "Message", Type = "String", Operator = FilterOperator.StartsWith, Value = "SSCU" }, + new ValueFilterDefinition { Field = "X", Type = "value", Operator = FilterOperator.Gt, Value = "10" }, + ] + }, }, + new AnalysisQuery { Title = "SSCU Cluster (0.25)", Dimensions = new [] { DimensionKind.X, DimensionKind.Y }, TopN = 50, IsDBScan = true, DbEps = 0.25, DbMinPts = 3, Filter = + new GroupFilterDefinition { Mode="And", Type = "group", Filters=[ + new ValueFilterDefinition { Field = "Message", Type = "String", Operator = FilterOperator.StartsWith, Value = "SSCU" }, + new ValueFilterDefinition { Field = "X", Type = "value", Operator = FilterOperator.Gt, Value = "10" }, + ] }, + } } }; } diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Models/Filters/FilterDefinitions.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Models/Filters/FilterDefinitions.cs index 4e0577214..8379558dd 100644 --- a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Models/Filters/FilterDefinitions.cs +++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Models/Filters/FilterDefinitions.cs @@ -9,7 +9,7 @@ namespace DataAnalysis.Core.Models; /// public abstract class FilterDefinition { - [JsonPropertyName("type")] public required string Type { get; init; } + [JsonPropertyName("type")] public string Type { get; init; } } public sealed class ValueFilterDefinition : FilterDefinition diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF.TestHarness/MainWindow.xaml.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF.TestHarness/MainWindow.xaml.cs index 4edbbfb24..493e11e41 100644 --- a/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF.TestHarness/MainWindow.xaml.cs +++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF.TestHarness/MainWindow.xaml.cs @@ -56,8 +56,8 @@ private void OnLoadClusters(object sender, RoutedEventArgs e) { [new Vector2(0, 0)] = 15000, [new Vector2(100000, 50000)] = 2000, - [new Vector2(-50000, 80000)] = 8000, - [new Vector2(800000, -230000)] = 300 + [new Vector2(-50000, 80000)] = 800, + [new Vector2(800000, -230000)] = 300000 }; var agg = new AggregationResult { diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/ViewModels/ClusterAggregationViewModel.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/ViewModels/ClusterAggregationViewModel.cs index ed5284a8b..312ede925 100644 --- a/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/ViewModels/ClusterAggregationViewModel.cs +++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/ViewModels/ClusterAggregationViewModel.cs @@ -8,40 +8,29 @@ namespace DataAnalysis.WPF.ViewModels; public sealed partial class ClusterAggregationViewModel : AggregationItemViewModel { - public sealed class PointItem - { - public double X { get; init; } - public double Y { get; init; } - public int Count { get; init; } - } + public sealed class PointItem { public double X { get; init; } public double Y { get; init; } public int Count { get; init; } } + public sealed class AxisTick { public double Value { get; init; } public string Label { get; init; } = string.Empty; } public ObservableCollection Points { get; } = new(); public ObservableCollection RawPoints { get; } = new(); + public ObservableCollection XTicks { get; } = new(); + public ObservableCollection YTicks { get; } = new(); - [ObservableProperty] - private double minX; - [ObservableProperty] - private double maxX; - [ObservableProperty] - private double minY; - [ObservableProperty] - private double maxY; - [ObservableProperty] - private int maxCount; + [ObservableProperty] private double minX; + [ObservableProperty] private double maxX; + [ObservableProperty] private double minY; + [ObservableProperty] private double maxY; + [ObservableProperty] private int maxCount; + [ObservableProperty] private bool hasZeroX; + [ObservableProperty] private bool hasZeroY; - public ClusterAggregationViewModel(AggregationResult agg) - : base(agg.Title) - { - Rebuild(agg); - } + public ClusterAggregationViewModel(AggregationResult agg) : base(agg.Title) { Rebuild(agg); } private void Rebuild(AggregationResult agg) { - Points.Clear(); - RawPoints.Clear(); + Points.Clear(); RawPoints.Clear(); XTicks.Clear(); YTicks.Clear(); double minx = double.PositiveInfinity, maxx = double.NegativeInfinity; - double miny = double.PositiveInfinity, maxy = double.NegativeInfinity; - int maxc = 0; + double miny = double.PositiveInfinity, maxy = double.NegativeInfinity; int maxc = 0; if (agg.Series is not null) { foreach (var kv in agg.Series) @@ -49,116 +38,77 @@ private void Rebuild(AggregationResult agg) if (TryGetXY(kv.Key, out var x, out var y)) { Points.Add(new PointItem { X = x, Y = y, Count = kv.Value }); - if (x < minx) - minx = x; - if (x > maxx) - maxx = x; - if (y < miny) - miny = y; - if (y > maxy) - maxy = y; - if (kv.Value > maxc) - maxc = kv.Value; + if (x < minx) minx = x; if (x > maxx) maxx = x; + if (y < miny) miny = y; if (y > maxy) maxy = y; + if (kv.Value > maxc) maxc = kv.Value; } } } - if (double.IsInfinity(minx) || double.IsInfinity(miny)) - { - minx = 0; - maxx = 1; - miny = 0; - maxy = 1; - maxc = 1; - } - MinX = minx; - MaxX = maxx; - MinY = miny; - MaxY = maxy; - MaxCount = maxc; + if (double.IsInfinity(minx) || double.IsInfinity(miny)) { minx = 0; maxx = 1; miny = 0; maxy = 1; maxc = 1; } + MinX = minx; MaxX = maxx; MinY = miny; MaxY = maxy; MaxCount = maxc; + HasZeroX = MinX <= 0 && MaxX >= 0; HasZeroY = MinY <= 0 && MaxY >= 0; + BuildTicks(XTicks, MinX, MaxX, 6); BuildTicks(YTicks, MinY, MaxY, 6); } - private static bool TryGetXY(object key, out double x, out double y) + private static void BuildTicks(ObservableCollection target, double min, double max, int desired) + { + target.Clear(); if (max <= min) { target.Add(new AxisTick { Value = min, Label = min.ToString("G4") }); return; } + var range = max - min; var rawStep = range / desired; double magnitude = System.Math.Pow(10, System.Math.Floor(System.Math.Log10(rawStep))); + double normalized = rawStep / magnitude; double step = normalized < 1.5 ? 1 * magnitude : normalized < 3 ? 2 * magnitude : normalized < 7 ? 5 * magnitude : 10 * magnitude; + double start = System.Math.Ceiling(min / step) * step; + for (double v = start; v <= max + step * 0.001; v += step) target.Add(new AxisTick { Value = v, Label = FormatTick(v, range) }); + if (min <= 0 && max >= 0 && !target.Any(t => System.Math.Abs(t.Value) < step * 0.001)) target.Add(new AxisTick { Value = 0, Label = FormatTick(0, range) }); + var ordered = target.OrderBy(t => t.Value).ToList(); if (ordered.Count != target.Count) { target.Clear(); foreach (var t in ordered) target.Add(t); } + } + + private static string FormatTick(double value, double range) { - x = y = 0; - if (key is null) - return false; - var t = key.GetType(); + if (range == 0) return value.ToString("G4"); var absRange = System.Math.Abs(range); + if (absRange >= 100000 || absRange <= 0.001) return value.ToString("G3"); + if (absRange >= 1000) return value.ToString("G4"); + if (absRange >= 1) return value.ToString("G5"); + return value.ToString("G3"); + } - //1) Properties named X/Y - var px = t.GetProperty("X"); - var py = t.GetProperty("Y"); - if (px is not null && py is not null) + private static bool TryGetXY(object key, out double x, out double y) + { + x = y = 0; if (key is null) return false; var t = key.GetType(); + var px = t.GetProperty("X"); var py = t.GetProperty("Y"); if (px is not null && py is not null) { - var vx = px.GetValue(key); - var vy = py.GetValue(key); - if (TryToDouble(vx, out x) && TryToDouble(vy, out y)) - return true; + var vx = px.GetValue(key); var vy = py.GetValue(key); + if (TryToDouble(vx, out x) && TryToDouble(vy, out y)) return true; } - - //2) Fields named X/Y (e.g., System.Numerics.Vector2) - var fx = t.GetField("X"); - var fy = t.GetField("Y"); - if (fx is not null && fy is not null) + var fx = t.GetField("X"); var fy = t.GetField("Y"); if (fx is not null && fy is not null) { - var vx = fx.GetValue(key); - var vy = fy.GetValue(key); - if (TryToDouble(vx, out x) && TryToDouble(vy, out y)) - return true; + var vx = fx.GetValue(key); var vy = fy.GetValue(key); + if (TryToDouble(vx, out x) && TryToDouble(vy, out y)) return true; } - - //3) ValueTuple or similar (Item1/Item2) if (t.IsGenericType && t.FullName!.StartsWith("System.ValueTuple`2")) { - var f1 = t.GetField("Item1"); - var f2 = t.GetField("Item2"); - if (f1 is not null && f2 is not null) + var f1 = t.GetField("Item1"); var f2 = t.GetField("Item2"); if (f1 is not null && f2 is not null) { - var v1 = f1.GetValue(key); - var v2 = f2.GetValue(key); - if (TryToDouble(v1, out x) && TryToDouble(v2, out y)) - return true; + var v1 = f1.GetValue(key); var v2 = f2.GetValue(key); + if (TryToDouble(v1, out x) && TryToDouble(v2, out y)) return true; } } - - //4) ITuple support if (key is ITuple tuple && tuple.Length >= 2) { - var v1 = tuple[0]; - var v2 = tuple[1]; - if (TryToDouble(v1, out x) && TryToDouble(v2, out y)) - return true; + var v1 = tuple[0]; var v2 = tuple[1]; + if (TryToDouble(v1, out x) && TryToDouble(v2, out y)) return true; } - - //5) double[] or similar length>=2 if (key is System.Collections.IList list && list.Count >= 2) { - var v1 = list[0]; - var v2 = list[1]; - if (TryToDouble(v1, out x) && TryToDouble(v2, out y)) - return true; + var v1 = list[0]; var v2 = list[1]; + if (TryToDouble(v1, out x) && TryToDouble(v2, out y)) return true; } - - //6) Fallback: parse from ToString like "(x,y)" - var s = key.ToString() ?? string.Empty; - var m = System.Text.RegularExpressions.Regex.Match(s, "\\(?\\s*(-?[0-9]+(?:\\.[0-9]+)?)\\s*,\\s*(-?[0-9]+(?:\\.[0-9]+)?)\\s*\\)?"); + var s = key.ToString() ?? string.Empty; var m = System.Text.RegularExpressions.Regex.Match(s, @"\(?\s*(-?[0-9]+(?:\.[0-9]+)?)\s*,\s*(-?[0-9]+(?:\.[0-9]+)?)\s*\)?"); if (m.Success) { if (double.TryParse(m.Groups[1].Value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out x) && - double.TryParse(m.Groups[2].Value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out y)) - { - return true; - } + double.TryParse(m.Groups[2].Value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out y)) return true; } return false; } - private static bool TryToDouble(object? v, out double d) - { - try - { - d = System.Convert.ToDouble(v, System.Globalization.CultureInfo.InvariantCulture); - return true; - } - catch { d = 0; return false; } - } + private static bool TryToDouble(object? v, out double d) { try { d = System.Convert.ToDouble(v, System.Globalization.CultureInfo.InvariantCulture); return true; } catch { d = 0; return false; } } } diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Controls/ClusterAggregationView.xaml b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Controls/ClusterAggregationView.xaml index a46ae7222..f3741c812 100644 --- a/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Controls/ClusterAggregationView.xaml +++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Controls/ClusterAggregationView.xaml @@ -1,155 +1,294 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/PlotConverters.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/PlotConverters.cs index 75433a164..ab2462271 100644 --- a/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/PlotConverters.cs +++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/PlotConverters.cs @@ -4,8 +4,10 @@ namespace DataAnalysis.WPF.Views.Converters; -public sealed class PlotXConverter : IMultiValueConverter +public sealed class PlotConverter : IMultiValueConverter { + // Half of maximum bubble size (SizeByCountConverter.MaxSize) to keep bubbles inside plot. + private const double EdgePadding = 60d; // MaxSize (120) / 2 public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { if (values is null || values.Length < 4) return 0d; @@ -15,9 +17,18 @@ public object Convert(object[] values, Type targetType, object parameter, Cultur var minX = System.Convert.ToDouble(values[1], CultureInfo.InvariantCulture); var maxX = System.Convert.ToDouble(values[2], CultureInfo.InvariantCulture); var width = System.Convert.ToDouble(values[3], CultureInfo.InvariantCulture); - if (maxX <= minX) return 0d; + var Offset = parameter != null?System.Convert.ToDouble(parameter, CultureInfo.InvariantCulture):0d; + if (width <= 0) return 0d; + if (maxX == minX) + { + // Degenerate range: place at center with padding consideration + var usable = Math.Max(0, width - 2 * EdgePadding); + return EdgePadding + usable / 2; + } + var usableWidth = Math.Max(0, width - 2 * EdgePadding); var t = (x - minX) / (maxX - minX); - return Math.Max(0, Math.Min(width, t * width)); + t = Math.Max(0, Math.Min(1, t)); + return EdgePadding + t * usableWidth+Offset; } catch { @@ -27,52 +38,3 @@ public object Convert(object[] values, Type targetType, object parameter, Cultur public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotSupportedException(); } - -public sealed class PlotYConverter : IMultiValueConverter -{ - public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) - { - if (values is null || values.Length < 4) return 0d; - try - { - var y = System.Convert.ToDouble(values[0], CultureInfo.InvariantCulture); - var minY = System.Convert.ToDouble(values[1], CultureInfo.InvariantCulture); - var maxY = System.Convert.ToDouble(values[2], CultureInfo.InvariantCulture); - var height = System.Convert.ToDouble(values[3], CultureInfo.InvariantCulture); - if (maxY <= minY) return 0d; - var t = (y - minY) / (maxY - minY); - return Math.Max(0, Math.Min(height, (1 - t) * height)); - } - catch - { - return 0d; - } - } - - public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotSupportedException(); -} - -public sealed class SizeByCountConverter : IMultiValueConverter -{ - public double MinSize { get; set; } = 6; - public double MaxSize { get; set; } = 120; - public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) - { - if (values is null || values.Length < 2) return MinSize; - try - { - var count = System.Convert.ToDouble(values[0], CultureInfo.InvariantCulture); - var max = System.Convert.ToDouble(values[1], CultureInfo.InvariantCulture); - if (max <= 0) return MinSize; - var t = Math.Max(0, Math.Min(1, count / max)); - t = Math.Sqrt(t); - return MinSize + (MaxSize - MinSize) * t; - } - catch - { - return MinSize; - } - } - - public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotSupportedException(); -} diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/PlotXCenteredConverter.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/PlotXCenteredConverter.cs new file mode 100644 index 000000000..dd5add01d --- /dev/null +++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/PlotXCenteredConverter.cs @@ -0,0 +1,50 @@ +using System.Globalization; +using System.Windows.Data; + +namespace DataAnalysis.WPF.Views.Converters; + +// Centered converters: return Canvas.Left/Top such that given (X,Y) is bubble center. +// Expect bindings: X|minX|maxX|width|count|maxCount (6 values) for X, similarly Y. +public sealed class PlotXCenteredConverter : IMultiValueConverter +{ + private const double EdgePadding = 60d; // Align with PlotConverter padding + private const double MinSize = 6d; + private const double MaxSize = 120d; + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (values is null || values.Length < 6) return 0d; + try + { + var x = System.Convert.ToDouble(values[0], CultureInfo.InvariantCulture); + var minX = System.Convert.ToDouble(values[1], CultureInfo.InvariantCulture); + var maxX = System.Convert.ToDouble(values[2], CultureInfo.InvariantCulture); + var width = System.Convert.ToDouble(values[3], CultureInfo.InvariantCulture); + var count = System.Convert.ToDouble(values[4], CultureInfo.InvariantCulture); + var maxCount = System.Convert.ToDouble(values[5], CultureInfo.InvariantCulture); + if (width <= 0) return 0d; + double size = MinSize; + if (maxCount > 0) + { + var tSize = Math.Max(0, Math.Min(1, count / maxCount)); + tSize = Math.Sqrt(tSize); + size = MinSize + (MaxSize - MinSize) * tSize; + } + var radius = size / 2d; + if (maxX == minX) + { + return Math.Max(0, Math.Min(width - size, width / 2d - radius)); + } + var usableWidth = Math.Max(0, width - 2 * EdgePadding); + var t = (x - minX) / (maxX - minX); + t = Math.Max(0, Math.Min(1, t)); + var center = EdgePadding + t * usableWidth; + var left = center - radius; + // Clamp to keep bubble fully visible + if (left < 0) left = 0; + if (left + size > width) left = Math.Max(0, width - size); + return left; + } + catch { return 0d; } + } + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotSupportedException(); +} diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/PlotYCenteredConverter.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/PlotYCenteredConverter.cs new file mode 100644 index 000000000..1cf1f2607 --- /dev/null +++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/PlotYCenteredConverter.cs @@ -0,0 +1,47 @@ +using System.Globalization; +using System.Windows.Data; + +namespace DataAnalysis.WPF.Views.Converters; + +public sealed class PlotYCenteredConverter : IMultiValueConverter +{ + private const double EdgePadding = 60d; + private const double MinSize = 6d; + private const double MaxSize = 120d; + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (values is null || values.Length < 6) return 0d; + try + { + var y = System.Convert.ToDouble(values[0], CultureInfo.InvariantCulture); + var minY = System.Convert.ToDouble(values[1], CultureInfo.InvariantCulture); + var maxY = System.Convert.ToDouble(values[2], CultureInfo.InvariantCulture); + var height = System.Convert.ToDouble(values[3], CultureInfo.InvariantCulture); + var count = System.Convert.ToDouble(values[4], CultureInfo.InvariantCulture); + var maxCount = System.Convert.ToDouble(values[5], CultureInfo.InvariantCulture); + if (height <= 0) return 0d; + double size = MinSize; + if (maxCount > 0) + { + var tSize = Math.Max(0, Math.Min(1, count / maxCount)); + tSize = Math.Sqrt(tSize); + size = MinSize + (MaxSize - MinSize) * tSize; + } + var radius = size / 2d; + if (maxY <= minY) + { + return Math.Max(0, Math.Min(height - size, height / 2d - radius)); + } + var usableHeight = Math.Max(0, height - 2 * EdgePadding); + var t = (y - minY) / (maxY - minY); + t = Math.Max(0, Math.Min(1, t)); + var center = EdgePadding + (1 - t) * usableHeight; // invert y + var top = center - radius; + if (top < 0) top = 0; + if (top + size > height) top = Math.Max(0, height - size); + return top; + } + catch { return 0d; } + } + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotSupportedException(); +} diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/SizeByCountConverter.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/SizeByCountConverter.cs new file mode 100644 index 000000000..1671dd3da --- /dev/null +++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/SizeByCountConverter.cs @@ -0,0 +1,25 @@ +using System.Globalization; +using System.Windows.Data; + +namespace DataAnalysis.WPF.Views.Converters; + +public sealed class SizeByCountConverter : IMultiValueConverter +{ + public double MinSize { get; set; } = 6; + public double MaxSize { get; set; } = 120; + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (values is null || values.Length < 2) return MinSize; + try + { + var count = System.Convert.ToDouble(values[0], CultureInfo.InvariantCulture); + var max = System.Convert.ToDouble(values[1], CultureInfo.InvariantCulture); + if (max <= 0) return MinSize; + var t = Math.Max(0, Math.Min(1, count / max)); + t = Math.Sqrt(t); + return MinSize + (MaxSize - MinSize) * t; + } + catch { return MinSize; } + } + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotSupportedException(); +} diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.sln b/CSharpBible/Data/DataAnalysis/DataAnalysis.sln index 3a5acb01e..ddc2dea06 100644 --- a/CSharpBible/Data/DataAnalysis/DataAnalysis.sln +++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.14.36616.10 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11205.157 d18.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataAnalysis.Core", "DataAnalysis.Core\DataAnalysis.Core.csproj", "{D8808BEF-2D64-4D46-903F-B1A0499D9F66}" EndProject @@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataAnalysis.WPF.TestHarnes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataConvert.Console", "DataConvert.Console\DataConvert.Console.csproj", "{E9D94D2B-42BA-4AD6-9CB2-D2D889B289FE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataAnalysis.Core.Tests", "DataAnalysis.Core.Tests\DataAnalysis.Core.Tests.csproj", "{DB99A65F-675C-E212-80B3-50A0C0D87138}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +41,10 @@ Global {E9D94D2B-42BA-4AD6-9CB2-D2D889B289FE}.Debug|Any CPU.Build.0 = Debug|Any CPU {E9D94D2B-42BA-4AD6-9CB2-D2D889B289FE}.Release|Any CPU.ActiveCfg = Release|Any CPU {E9D94D2B-42BA-4AD6-9CB2-D2D889B289FE}.Release|Any CPU.Build.0 = Release|Any CPU + {DB99A65F-675C-E212-80B3-50A0C0D87138}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB99A65F-675C-E212-80B3-50A0C0D87138}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB99A65F-675C-E212-80B3-50A0C0D87138}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB99A65F-675C-E212-80B3-50A0C0D87138}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/CSharpBible/Data/DataAnalysis/DataConvert.Console/App.cs b/CSharpBible/Data/DataAnalysis/DataConvert.Console/App.cs index 90c015545..53d2923d0 100644 --- a/CSharpBible/Data/DataAnalysis/DataConvert.Console/App.cs +++ b/CSharpBible/Data/DataAnalysis/DataConvert.Console/App.cs @@ -18,6 +18,7 @@ public App() { // Exporter: table only services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(sp => new DelimitedTableParsingProfile { @@ -44,6 +45,8 @@ public App() .Build(); } + IConsoleWriter? console; + public async Task RunAsync(string[] args) { await _host.StartAsync(); @@ -57,10 +60,12 @@ public async Task RunAsync(string[] args) var inputPath = args[0]; var tableReader = _host.Services.GetRequiredService(); var exporter = _host.Services.GetRequiredService(); - - var table = await tableReader.ReadTableAsync(inputPath, CancellationToken.None); - var output = await exporter.ExportAsync(table, inputPath, null, CancellationToken.None); - System.Console.WriteLine(output); + console = _host.Services.GetRequiredService(); + console.WriteLine("Reading ..."); + var table = await tableReader.ReadTableAsync(inputPath, CancellationToken.None,onProgress); + console.WriteLine("\nConverting ..."); + var output = await exporter.ExportAsync(table, inputPath, null, CancellationToken.None,onProgress); + console.WriteLine("\n"+output); return 0; } catch (Exception ex) @@ -74,4 +79,14 @@ public async Task RunAsync(string[] args) _host.Dispose(); } } + + private void onProgress(double obj) + { + console.Write("["); + int totalBlocks = 70; + int filledBlocks = (int)(obj * totalBlocks); + console.Write(new string('#', filledBlocks)); + console.Write(new string('-', totalBlocks - filledBlocks)); + console.Write($"] {obj:P0}\r"); + } } diff --git a/CSharpBible/Data/DataAnalysis/DataConvert.Console/ConsoleWriter.cs b/CSharpBible/Data/DataAnalysis/DataConvert.Console/ConsoleWriter.cs new file mode 100644 index 000000000..f5aa8739c --- /dev/null +++ b/CSharpBible/Data/DataAnalysis/DataConvert.Console/ConsoleWriter.cs @@ -0,0 +1,14 @@ +namespace DataConvert.Console; + +public class ConsoleWriter : IConsoleWriter +{ + public void Write(string v) + { + System.Console.Write(v); + } + + public void WriteLine(string v) + { + System.Console.WriteLine(v); + } +} \ No newline at end of file diff --git a/CSharpBible/Data/DataAnalysis/DataConvert.Console/DataConvert.Console.csproj b/CSharpBible/Data/DataAnalysis/DataConvert.Console/DataConvert.Console.csproj index 70799f400..6af1c927c 100644 --- a/CSharpBible/Data/DataAnalysis/DataConvert.Console/DataConvert.Console.csproj +++ b/CSharpBible/Data/DataAnalysis/DataConvert.Console/DataConvert.Console.csproj @@ -7,6 +7,10 @@ enable + + + + @@ -15,4 +19,10 @@ + + + PreserveNewest + + + diff --git a/CSharpBible/Data/DataAnalysis/DataConvert.Console/IConsoleWriter.cs b/CSharpBible/Data/DataAnalysis/DataConvert.Console/IConsoleWriter.cs new file mode 100644 index 000000000..414f7e8f5 --- /dev/null +++ b/CSharpBible/Data/DataAnalysis/DataConvert.Console/IConsoleWriter.cs @@ -0,0 +1,7 @@ +namespace DataConvert.Console; + +internal interface IConsoleWriter +{ + void Write(string v); + void WriteLine(string v); +} \ No newline at end of file diff --git a/CSharpBible/Data/DataAnalysis/DataConvert.Console/Ressources/.info b/CSharpBible/Data/DataAnalysis/DataConvert.Console/Ressources/.info new file mode 100644 index 000000000..e69de29bb diff --git a/CSharpBible/MVVM_Tutorial/MVVM_25_RichTextEdit.sln b/CSharpBible/MVVM_Tutorial/MVVM_25_RichTextEdit.sln index b533bdb3f..bd78c49ad 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_25_RichTextEdit.sln +++ b/CSharpBible/MVVM_Tutorial/MVVM_25_RichTextEdit.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11205.157 d18.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Projektmappenelemente", "Projektmappenelemente", "{658BD492-33FF-4995-AAE7-4F6894E92F0C}" ProjectSection(SolutionItems) = preProject @@ -30,6 +30,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommonDialogs", "..\Librari EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommonDialogs_net", "..\Libraries\CommonDialogs\CommonDialogs_net.csproj", "{7CF63553-BD9F-4999-B6D8-669405EA230E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MVVM_25a_CTRichTextEdit", "MVVM_25_CTRichTextEdit\MVVM_25a_CTRichTextEdit.csproj", "{83BA34EF-0E3B-A999-B15B-8757CD718FF0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MVVM_25a_CTRichTextEdit_net", "MVVM_25_CTRichTextEdit\MVVM_25a_CTRichTextEdit_net.csproj", "{2946216E-E3F5-CDF0-45EC-A1B03BD3BC9B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -102,6 +106,22 @@ Global {7CF63553-BD9F-4999-B6D8-669405EA230E}.Release|Any CPU.Build.0 = Release|Any CPU {7CF63553-BD9F-4999-B6D8-669405EA230E}.Release|x86.ActiveCfg = Release|x86 {7CF63553-BD9F-4999-B6D8-669405EA230E}.Release|x86.Build.0 = Release|x86 + {83BA34EF-0E3B-A999-B15B-8757CD718FF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {83BA34EF-0E3B-A999-B15B-8757CD718FF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {83BA34EF-0E3B-A999-B15B-8757CD718FF0}.Debug|x86.ActiveCfg = Debug|Any CPU + {83BA34EF-0E3B-A999-B15B-8757CD718FF0}.Debug|x86.Build.0 = Debug|Any CPU + {83BA34EF-0E3B-A999-B15B-8757CD718FF0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {83BA34EF-0E3B-A999-B15B-8757CD718FF0}.Release|Any CPU.Build.0 = Release|Any CPU + {83BA34EF-0E3B-A999-B15B-8757CD718FF0}.Release|x86.ActiveCfg = Release|Any CPU + {83BA34EF-0E3B-A999-B15B-8757CD718FF0}.Release|x86.Build.0 = Release|Any CPU + {2946216E-E3F5-CDF0-45EC-A1B03BD3BC9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2946216E-E3F5-CDF0-45EC-A1B03BD3BC9B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2946216E-E3F5-CDF0-45EC-A1B03BD3BC9B}.Debug|x86.ActiveCfg = Debug|Any CPU + {2946216E-E3F5-CDF0-45EC-A1B03BD3BC9B}.Debug|x86.Build.0 = Debug|Any CPU + {2946216E-E3F5-CDF0-45EC-A1B03BD3BC9B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2946216E-E3F5-CDF0-45EC-A1B03BD3BC9B}.Release|Any CPU.Build.0 = Release|Any CPU + {2946216E-E3F5-CDF0-45EC-A1B03BD3BC9B}.Release|x86.ActiveCfg = Release|Any CPU + {2946216E-E3F5-CDF0-45EC-A1B03BD3BC9B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/TestStatements/AppWithPlugin/AppWithPlugin.csproj b/TestStatements/AppWithPlugin/AppWithPlugin.csproj index 70348bf04..c19edbe79 100644 --- a/TestStatements/AppWithPlugin/AppWithPlugin.csproj +++ b/TestStatements/AppWithPlugin/AppWithPlugin.csproj @@ -19,9 +19,9 @@ - + - + diff --git a/TestStatements/AppWithPluginWpf/AppWithPluginWpf.csproj b/TestStatements/AppWithPluginWpf/AppWithPluginWpf.csproj index d64900ed4..07f0839ae 100644 --- a/TestStatements/AppWithPluginWpf/AppWithPluginWpf.csproj +++ b/TestStatements/AppWithPluginWpf/AppWithPluginWpf.csproj @@ -17,9 +17,9 @@ - + - + diff --git a/TestStatements/AsyncExampleWPF/README.md b/TestStatements/AsyncExampleWPF/README.md new file mode 100644 index 000000000..47f5fe5eb --- /dev/null +++ b/TestStatements/AsyncExampleWPF/README.md @@ -0,0 +1,22 @@ +# AsyncExampleWPF + +Multi-targeted WPF sample application (net462; net472; net48; net481) demonstrating asynchronous programming patterns in a desktop UI context. + +## Goals +- Showcase async/await integration with WPF message loop +- Illustrate responsiveness improvements vs synchronous blocking +- Provide a baseline for comparing legacy framework behaviors + +## Target Frameworks +Classic .NET Framework TFMs only; no .NET (Core) targets—useful to contrast with newer projects in the solution. + +## Features (expected in code-behind / ViewModels) +- Async event handlers (e.g., button click triggering background operations) +- UI thread marshaling via SynchronizationContext / Dispatcher +- Potential cancellation token usage + +## Build +`dotnet build AsyncExampleWPF/AsyncExampleWPF.csproj` + +## Extensibility +Project can be upgraded to multi-target .NET 6+ WPF by adding windows TFMs (e.g., net8.0-windows) if desired. diff --git a/TestStatements/HelloPlugin/HelloPlugin.csproj b/TestStatements/HelloPlugin/HelloPlugin.csproj index 5de2be730..55d3b5677 100644 --- a/TestStatements/HelloPlugin/HelloPlugin.csproj +++ b/TestStatements/HelloPlugin/HelloPlugin.csproj @@ -19,7 +19,7 @@ - + diff --git a/TestStatements/HelloPluginTest/HelloPluginTest.csproj b/TestStatements/HelloPluginTest/HelloPluginTest.csproj index 037dd011e..62eed8838 100644 --- a/TestStatements/HelloPluginTest/HelloPluginTest.csproj +++ b/TestStatements/HelloPluginTest/HelloPluginTest.csproj @@ -11,7 +11,7 @@ - + diff --git a/TestStatements/OtherPlugin/OtherPlugin.csproj b/TestStatements/OtherPlugin/OtherPlugin.csproj index cbb3107bf..78325ffb4 100644 --- a/TestStatements/OtherPlugin/OtherPlugin.csproj +++ b/TestStatements/OtherPlugin/OtherPlugin.csproj @@ -15,7 +15,7 @@ ..\snKey.snk - + diff --git a/TestStatements/TestStatements/TestStatements.csproj b/TestStatements/TestStatements/TestStatements.csproj index a1e1a41ed..9bcc4a375 100644 --- a/TestStatements/TestStatements/TestStatements.csproj +++ b/TestStatements/TestStatements/TestStatements.csproj @@ -56,10 +56,10 @@ - + - - + + diff --git a/TestStatements/TestStatements/TestStatements_net.csproj b/TestStatements/TestStatements/TestStatements_net.csproj index bd7a142fd..9174fd38b 100644 --- a/TestStatements/TestStatements/TestStatements_net.csproj +++ b/TestStatements/TestStatements/TestStatements_net.csproj @@ -56,9 +56,9 @@ - - - + + + diff --git a/Transpiler_pp/Analyzer1/Analyzer1.CodeFixes/Analyzer1.CodeFixes.csproj b/Transpiler_pp/Analyzer1/Analyzer1.CodeFixes/Analyzer1.CodeFixes.csproj index 31864fc10..23ffa980a 100644 --- a/Transpiler_pp/Analyzer1/Analyzer1.CodeFixes/Analyzer1.CodeFixes.csproj +++ b/Transpiler_pp/Analyzer1/Analyzer1.CodeFixes/Analyzer1.CodeFixes.csproj @@ -7,7 +7,7 @@ - + diff --git a/Transpiler_pp/Analyzer1/Analyzer1/Analyzer1.csproj b/Transpiler_pp/Analyzer1/Analyzer1/Analyzer1.csproj index 6d1da0b9c..f57f4112d 100644 --- a/Transpiler_pp/Analyzer1/Analyzer1/Analyzer1.csproj +++ b/Transpiler_pp/Analyzer1/Analyzer1/Analyzer1.csproj @@ -15,7 +15,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Transpiler_pp/Transpiler.sln b/Transpiler_pp/Transpiler.sln index 64a695415..625adcb41 100644 --- a/Transpiler_pp/Transpiler.sln +++ b/Transpiler_pp/Transpiler.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 -VisualStudioVersion = 18.0.11201.2 d18.0 +VisualStudioVersion = 18.0.11201.2 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Projektmappenelemente", "Projektmappenelemente", "{FB9836CD-815C-4901-B10D-8CCF99B18E30}" ProjectSection(SolutionItems) = preProject @@ -30,14 +30,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzer1.CodeFixes", "Anal EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzer1.Package", "Analyzer1\Analyzer1.Package\Analyzer1.Package.csproj", "{15FE2AB9-637E-4FD2-BB48-67C3CE22DEBD}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzer1.Test", "Analyzer1\Analyzer1.Test\Analyzer1.Test.csproj", "{29649B15-C865-4DFF-A2C6-D4A396DF9578}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzer1.Vsix", "Analyzer1\Analyzer1.Vsix\Analyzer1.Vsix.csproj", "{8A24ABD1-0D57-4EFD-8C2A-4999D4DACAE0}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VBUnObfusicator", "..\Gen_FreeWin\VBUnObfusicator\VBUnObfusicator.csproj", "{4D2FE91E-8018-4CB7-9778-5648833E883D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VBUnObfusicatorTests", "..\Gen_FreeWin\VBUnObfusicatorTests\VBUnObfusicatorTests.csproj", "{EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TranspilerLibTests", "TranspilerLibTests\TranspilerLibTests.csproj", "{C87BEC24-2C21-4DEF-BB8D-B9503105A086}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseLib", "..\CSharpBible\Libraries\BaseLib\BaseLib.csproj", "{8608004D-250B-461E-BD95-D288414A9C01}" @@ -52,6 +46,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TranspilerLib.CSharp", "Tra EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TranspilerLib.Pascal.Tests", "TranspilerLib.Pascal.Tests\TranspilerLib.Pascal.Tests.csproj", "{DD21FC5F-2FFF-4BD6-847B-7B5006DB3389}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cmd", "cmd", "{0E2C6B7C-A659-45E8-AD73-092473A0572D}" + ProjectSection(SolutionItems) = preProject + ..\..\Cmd\CheckIn2.cmd = ..\..\Cmd\CheckIn2.cmd + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -146,18 +145,6 @@ Global {15FE2AB9-637E-4FD2-BB48-67C3CE22DEBD}.Release|Mixed Platforms.Build.0 = Release|Any CPU {15FE2AB9-637E-4FD2-BB48-67C3CE22DEBD}.Release|x86.ActiveCfg = Release|Any CPU {15FE2AB9-637E-4FD2-BB48-67C3CE22DEBD}.Release|x86.Build.0 = Release|Any CPU - {29649B15-C865-4DFF-A2C6-D4A396DF9578}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {29649B15-C865-4DFF-A2C6-D4A396DF9578}.Debug|Any CPU.Build.0 = Debug|Any CPU - {29649B15-C865-4DFF-A2C6-D4A396DF9578}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {29649B15-C865-4DFF-A2C6-D4A396DF9578}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {29649B15-C865-4DFF-A2C6-D4A396DF9578}.Debug|x86.ActiveCfg = Debug|Any CPU - {29649B15-C865-4DFF-A2C6-D4A396DF9578}.Debug|x86.Build.0 = Debug|Any CPU - {29649B15-C865-4DFF-A2C6-D4A396DF9578}.Release|Any CPU.ActiveCfg = Release|Any CPU - {29649B15-C865-4DFF-A2C6-D4A396DF9578}.Release|Any CPU.Build.0 = Release|Any CPU - {29649B15-C865-4DFF-A2C6-D4A396DF9578}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {29649B15-C865-4DFF-A2C6-D4A396DF9578}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {29649B15-C865-4DFF-A2C6-D4A396DF9578}.Release|x86.ActiveCfg = Release|Any CPU - {29649B15-C865-4DFF-A2C6-D4A396DF9578}.Release|x86.Build.0 = Release|Any CPU {8A24ABD1-0D57-4EFD-8C2A-4999D4DACAE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8A24ABD1-0D57-4EFD-8C2A-4999D4DACAE0}.Debug|Any CPU.Build.0 = Debug|Any CPU {8A24ABD1-0D57-4EFD-8C2A-4999D4DACAE0}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -170,30 +157,6 @@ Global {8A24ABD1-0D57-4EFD-8C2A-4999D4DACAE0}.Release|Mixed Platforms.Build.0 = Release|Any CPU {8A24ABD1-0D57-4EFD-8C2A-4999D4DACAE0}.Release|x86.ActiveCfg = Release|Any CPU {8A24ABD1-0D57-4EFD-8C2A-4999D4DACAE0}.Release|x86.Build.0 = Release|Any CPU - {4D2FE91E-8018-4CB7-9778-5648833E883D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4D2FE91E-8018-4CB7-9778-5648833E883D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4D2FE91E-8018-4CB7-9778-5648833E883D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {4D2FE91E-8018-4CB7-9778-5648833E883D}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {4D2FE91E-8018-4CB7-9778-5648833E883D}.Debug|x86.ActiveCfg = Debug|Any CPU - {4D2FE91E-8018-4CB7-9778-5648833E883D}.Debug|x86.Build.0 = Debug|Any CPU - {4D2FE91E-8018-4CB7-9778-5648833E883D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4D2FE91E-8018-4CB7-9778-5648833E883D}.Release|Any CPU.Build.0 = Release|Any CPU - {4D2FE91E-8018-4CB7-9778-5648833E883D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {4D2FE91E-8018-4CB7-9778-5648833E883D}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {4D2FE91E-8018-4CB7-9778-5648833E883D}.Release|x86.ActiveCfg = Release|Any CPU - {4D2FE91E-8018-4CB7-9778-5648833E883D}.Release|x86.Build.0 = Release|Any CPU - {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}.Debug|x86.ActiveCfg = Debug|Any CPU - {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}.Debug|x86.Build.0 = Debug|Any CPU - {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}.Release|Any CPU.Build.0 = Release|Any CPU - {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}.Release|x86.ActiveCfg = Release|Any CPU - {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}.Release|x86.Build.0 = Release|Any CPU {C87BEC24-2C21-4DEF-BB8D-B9503105A086}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C87BEC24-2C21-4DEF-BB8D-B9503105A086}.Debug|Any CPU.Build.0 = Debug|Any CPU {C87BEC24-2C21-4DEF-BB8D-B9503105A086}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -290,10 +253,7 @@ Global {68CCC629-61B7-435B-A39C-C9EE023388D6} = {5481B009-5D62-45A8-9DD3-B9E6792B0C0E} {BE43CB35-C6AA-4EA3-BE1D-8DC865B948A8} = {5481B009-5D62-45A8-9DD3-B9E6792B0C0E} {15FE2AB9-637E-4FD2-BB48-67C3CE22DEBD} = {5481B009-5D62-45A8-9DD3-B9E6792B0C0E} - {29649B15-C865-4DFF-A2C6-D4A396DF9578} = {5481B009-5D62-45A8-9DD3-B9E6792B0C0E} {8A24ABD1-0D57-4EFD-8C2A-4999D4DACAE0} = {5481B009-5D62-45A8-9DD3-B9E6792B0C0E} - {4D2FE91E-8018-4CB7-9778-5648833E883D} = {5481B009-5D62-45A8-9DD3-B9E6792B0C0E} - {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD} = {5481B009-5D62-45A8-9DD3-B9E6792B0C0E} {C87BEC24-2C21-4DEF-BB8D-B9503105A086} = {5481B009-5D62-45A8-9DD3-B9E6792B0C0E} {8608004D-250B-461E-BD95-D288414A9C01} = {8655F273-B5AF-4D9A-B635-7F9168703579} {893755DA-9A1A-4DCB-8B7D-CD5C61790B76} = {8655F273-B5AF-4D9A-B635-7F9168703579}