From 4bc3951364441a96914b664ebffca5d909d119f4 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Thu, 9 Apr 2026 19:00:46 +0200 Subject: [PATCH 01/96] DAO --- .../BaseGenClasses/Model/GenTransaction.cs | 90 +++++++++++++++++++ .../Persistence/FactJournalValue.cs | 64 +++++++++++++ .../Persistence/GenealogyJournalExtensions.cs | 57 ++++++++++++ .../Persistence/IGenealogyJournalContext.cs | 32 +++++++ .../JournalEntryRecordedEventArgs.cs | 24 +++++ .../Persistence/MediaJournalValue.cs | 52 +++++++++++ .../Persistence/RepositoryJournalValue.cs | 51 +++++++++++ 7 files changed, 370 insertions(+) create mode 100644 WinAhnenNew/BaseGenClasses/Model/GenTransaction.cs create mode 100644 WinAhnenNew/BaseGenClasses/Persistence/FactJournalValue.cs create mode 100644 WinAhnenNew/BaseGenClasses/Persistence/GenealogyJournalExtensions.cs create mode 100644 WinAhnenNew/BaseGenClasses/Persistence/IGenealogyJournalContext.cs create mode 100644 WinAhnenNew/BaseGenClasses/Persistence/JournalEntryRecordedEventArgs.cs create mode 100644 WinAhnenNew/BaseGenClasses/Persistence/MediaJournalValue.cs create mode 100644 WinAhnenNew/BaseGenClasses/Persistence/RepositoryJournalValue.cs diff --git a/WinAhnenNew/BaseGenClasses/Model/GenTransaction.cs b/WinAhnenNew/BaseGenClasses/Model/GenTransaction.cs new file mode 100644 index 000000000..d71211545 --- /dev/null +++ b/WinAhnenNew/BaseGenClasses/Model/GenTransaction.cs @@ -0,0 +1,90 @@ +using BaseGenClasses.Helper; +using GenInterfaces.Data; +using GenInterfaces.Interfaces; +using GenInterfaces.Interfaces.Genealogic; +using System; +using System.Text.Json.Serialization; + +namespace BaseGenClasses.Model; + +/// +/// Represents a genealogy journal transaction. +/// +[JsonDerivedType(typeof(GenTransaction), typeDiscriminator: nameof(GenTransaction))] +public sealed class GenTransaction : GenObject, IGenTransaction, IHasOwner +{ + private object? _owner; + private IGenTransaction? _next; + + /// + /// Initializes a new instance of the class. + /// + public GenTransaction() + { + Items = new IndexedList(static objItem => objItem.GetHashCode().ToString(System.Globalization.CultureInfo.InvariantCulture)); + } + + /// + public override EGenType eGenType => EGenType.Genealogy; + + /// + public IGenBase Class { get; init; } = null!; + + /// + public IGenBase Entry { get; init; } = null!; + + /// + public object? Data { get; init; } + + /// + public object? OldData { get; init; } + + /// + public DateTime Timestamp { get; init; } = DateTime.UtcNow; + + /// + public IGenTransaction? Prev { get; init; } + + /// + [JsonIgnore] + public IIndexedList Items { get; } + + /// + [JsonIgnore] + public IGenTransaction Next => _next ?? this; + + /// + [JsonIgnore] + public IGenTransaction First => Prev?.First ?? this; + + /// + [JsonIgnore] + public IGenTransaction Last => _next?.Last ?? this; + + /// + [JsonIgnore] + public bool IsFirst => Prev is null; + + /// + [JsonIgnore] + public bool IsLast => _next is null; + + /// + [JsonIgnore] + public object? Owner => _owner; + + /// + /// Links the current transaction to a subsequent one. + /// + /// The next transaction. + public void SetNext(IGenTransaction? genNext) + { + _next = genNext; + } + + /// + public void SetOwner(object tOwner) + { + _owner = tOwner; + } +} diff --git a/WinAhnenNew/BaseGenClasses/Persistence/FactJournalValue.cs b/WinAhnenNew/BaseGenClasses/Persistence/FactJournalValue.cs new file mode 100644 index 000000000..4ab150b2f --- /dev/null +++ b/WinAhnenNew/BaseGenClasses/Persistence/FactJournalValue.cs @@ -0,0 +1,64 @@ +using System; +using GenInterfaces.Data; +using GenInterfaces.Interfaces.Genealogic; + +namespace BaseGenClasses.Persistence; + +/// +/// Represents a serializable fact snapshot for genealogy journal entries. +/// +public sealed class FactJournalValue +{ + /// + /// Gets or sets the fact type. + /// + public EFactType eFactType { get; init; } + + /// + /// Gets or sets the fact data text. + /// + public string? Data { get; init; } + + /// + /// Gets or sets the primary event date. + /// + public DateTime? Date1 { get; init; } + + /// + /// Gets or sets the date modifier. + /// + public EDateModifier? eDateModifier { get; init; } + + /// + /// Gets or sets the display date text. + /// + public string? DateText { get; init; } + + /// + /// Gets or sets the place name. + /// + public string? PlaceName { get; init; } + + /// + /// Creates a journal snapshot from a genealogy fact. + /// + /// The fact to snapshot. + /// The snapshot value, or when no fact is available. + public static FactJournalValue? FromFact(IGenFact? genFact) + { + if (genFact is null) + { + return null; + } + + return new FactJournalValue + { + eFactType = genFact.eFactType, + Data = genFact.Data, + Date1 = genFact.Date?.Date1 == DateTime.MinValue ? null : genFact.Date?.Date1, + eDateModifier = genFact.Date?.eDateModifier, + DateText = genFact.Date?.DateText, + PlaceName = genFact.Place?.Name + }; + } +} diff --git a/WinAhnenNew/BaseGenClasses/Persistence/GenealogyJournalExtensions.cs b/WinAhnenNew/BaseGenClasses/Persistence/GenealogyJournalExtensions.cs new file mode 100644 index 000000000..3c21b5904 --- /dev/null +++ b/WinAhnenNew/BaseGenClasses/Persistence/GenealogyJournalExtensions.cs @@ -0,0 +1,57 @@ +using GenInterfaces.Interfaces.Genealogic; + +namespace BaseGenClasses.Persistence; + +/// +/// Provides helper methods for recording typed genealogy journal entries. +/// +public static class GenealogyJournalExtensions +{ + /// + /// Records a journal entry for a source change. + /// + /// The genealogy journal context. + /// The changed source. + /// The new source snapshot. + /// The old source snapshot. + public static void RecordSourceChange( + this IGenealogyJournalContext journalContext, + IGenSource genSource, + SourceJournalValue? srcNewValue, + SourceJournalValue? srcOldValue) + { + journalContext.RecordJournalEntry(genSource, genSource, srcNewValue, srcOldValue); + } + + /// + /// Records a journal entry for a media change. + /// + /// The genealogy journal context. + /// The changed media item. + /// The new media snapshot. + /// The old media snapshot. + public static void RecordMediaChange( + this IGenealogyJournalContext journalContext, + IGenMedia genMedia, + MediaJournalValue? medNewValue, + MediaJournalValue? medOldValue) + { + journalContext.RecordJournalEntry(genMedia, genMedia, medNewValue, medOldValue); + } + + /// + /// Records a journal entry for a repository change. + /// + /// The genealogy journal context. + /// The changed repository. + /// The new repository snapshot. + /// The old repository snapshot. + public static void RecordRepositoryChange( + this IGenealogyJournalContext journalContext, + IGenRepository genRepository, + RepositoryJournalValue? repNewValue, + RepositoryJournalValue? repOldValue) + { + journalContext.RecordJournalEntry(genRepository, genRepository, repNewValue, repOldValue); + } +} diff --git a/WinAhnenNew/BaseGenClasses/Persistence/IGenealogyJournalContext.cs b/WinAhnenNew/BaseGenClasses/Persistence/IGenealogyJournalContext.cs new file mode 100644 index 000000000..313cda680 --- /dev/null +++ b/WinAhnenNew/BaseGenClasses/Persistence/IGenealogyJournalContext.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using GenInterfaces.Interfaces; +using GenInterfaces.Interfaces.Genealogic; + +namespace BaseGenClasses.Persistence; + +/// +/// Exposes recorded journal entries for a genealogy root context. +/// +public interface IGenealogyJournalContext +{ + /// + /// Occurs when a new journal entry was recorded. + /// + event EventHandler? JournalEntryRecorded; + + /// + /// Gets the recorded journal entries. + /// + IReadOnlyList JournalEntries { get; } + + /// + /// Records a journal entry for a genealogy change. + /// + /// The changed aggregate or class. + /// The changed entry. + /// The new value snapshot. + /// The old value snapshot. + /// The recorded transaction. + IGenTransaction RecordJournalEntry(IGenBase genClass, IGenBase genEntry, object? objData, object? objOldData); +} diff --git a/WinAhnenNew/BaseGenClasses/Persistence/JournalEntryRecordedEventArgs.cs b/WinAhnenNew/BaseGenClasses/Persistence/JournalEntryRecordedEventArgs.cs new file mode 100644 index 000000000..f3a1c42e0 --- /dev/null +++ b/WinAhnenNew/BaseGenClasses/Persistence/JournalEntryRecordedEventArgs.cs @@ -0,0 +1,24 @@ +using System; +using GenInterfaces.Interfaces; + +namespace BaseGenClasses.Persistence; + +/// +/// Provides details about a recorded genealogy journal entry. +/// +public sealed class JournalEntryRecordedEventArgs : EventArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// The recorded journal transaction. + public JournalEntryRecordedEventArgs(IGenTransaction genTransaction) + { + GenTransaction = genTransaction ?? throw new ArgumentNullException(nameof(genTransaction)); + } + + /// + /// Gets the recorded journal transaction. + /// + public IGenTransaction GenTransaction { get; } +} diff --git a/WinAhnenNew/BaseGenClasses/Persistence/MediaJournalValue.cs b/WinAhnenNew/BaseGenClasses/Persistence/MediaJournalValue.cs new file mode 100644 index 000000000..549e8eb16 --- /dev/null +++ b/WinAhnenNew/BaseGenClasses/Persistence/MediaJournalValue.cs @@ -0,0 +1,52 @@ +using System; +using GenInterfaces.Data; +using GenInterfaces.Interfaces.Genealogic; + +namespace BaseGenClasses.Persistence; + +/// +/// Represents a serializable media snapshot for genealogy journal entries. +/// +public sealed class MediaJournalValue +{ + /// + /// Gets or sets the media type. + /// + public EMediaType eMediaType { get; init; } + + /// + /// Gets or sets the media URI. + /// + public string? MediaUri { get; init; } + + /// + /// Gets or sets the display name of the media. + /// + public string? MediaName { get; init; } + + /// + /// Gets or sets the description of the media. + /// + public string? MediaDescription { get; init; } + + /// + /// Creates a journal snapshot from a genealogy media item. + /// + /// The media item to snapshot. + /// The snapshot value, or when no media item is available. + public static MediaJournalValue? FromMedia(IGenMedia? genMedia) + { + if (genMedia is null) + { + return null; + } + + return new MediaJournalValue + { + eMediaType = genMedia.eMediaType, + MediaUri = genMedia.MediaUri?.ToString(), + MediaName = genMedia.MediaName, + MediaDescription = genMedia.MediaDescription + }; + } +} diff --git a/WinAhnenNew/BaseGenClasses/Persistence/RepositoryJournalValue.cs b/WinAhnenNew/BaseGenClasses/Persistence/RepositoryJournalValue.cs new file mode 100644 index 000000000..a7eb2a9ac --- /dev/null +++ b/WinAhnenNew/BaseGenClasses/Persistence/RepositoryJournalValue.cs @@ -0,0 +1,51 @@ +using System; +using GenInterfaces.Interfaces.Genealogic; + +namespace BaseGenClasses.Persistence; + +/// +/// Represents a serializable repository snapshot for genealogy journal entries. +/// +public sealed class RepositoryJournalValue +{ + /// + /// Gets or sets the repository name. + /// + public string? Name { get; init; } + + /// + /// Gets or sets the repository information text. + /// + public string? Info { get; init; } + + /// + /// Gets or sets the repository URI. + /// + public string? Uri { get; init; } + + /// + /// Gets or sets the number of linked sources. + /// + public int SourceCount { get; init; } + + /// + /// Creates a journal snapshot from a genealogy repository. + /// + /// The repository to snapshot. + /// The snapshot value, or when no repository is available. + public static RepositoryJournalValue? FromRepository(IGenRepository? genRepository) + { + if (genRepository is null) + { + return null; + } + + return new RepositoryJournalValue + { + Name = genRepository.Name, + Info = genRepository.Info, + Uri = genRepository.Uri?.ToString(), + SourceCount = genRepository.GenSources.Count + }; + } +} From 55e9c1784e0133905d9a0d4e855eceff62278d50 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Thu, 9 Apr 2026 19:00:55 +0200 Subject: [PATCH 02/96] BaseGenClasses --- WinAhnenNew/BaseGenClasses/Model/Genealogy.cs | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/WinAhnenNew/BaseGenClasses/Model/Genealogy.cs b/WinAhnenNew/BaseGenClasses/Model/Genealogy.cs index 81fd98eda..2f36d3f25 100644 --- a/WinAhnenNew/BaseGenClasses/Model/Genealogy.cs +++ b/WinAhnenNew/BaseGenClasses/Model/Genealogy.cs @@ -15,7 +15,7 @@ namespace BaseGenClasses.Model; -public class Genealogy : IGenealogy, IGenealogyPersistenceContext, IRecipient, IDisposable +public class Genealogy : IGenealogy, IGenealogyPersistenceContext, IGenealogyJournalContext, IRecipient, IDisposable { private readonly IMessenger _messanger; private IGenealogyPersistenceProvider? _persistenceProvider; @@ -51,6 +51,10 @@ public class Genealogy : IGenealogy, IGenealogyPersistenceContext, IRecipient? FlushFailed; + public event EventHandler? JournalEntryRecorded; + + public IReadOnlyList JournalEntries => Transactions.ToArray(); + #endregion #region Methods @@ -195,16 +199,54 @@ private IGenTransaction _GetTransaction(IList o) public void Receive(IGenTransaction message) { - var _lastTA = Transactions.Where(ta => ta.Class == message.Class && ta.Entry == message.Entry).LastOrDefault(); - Transactions.Add(message); + RecordIncomingTransaction(message); MarkDirty(null, "A genealogy transaction was recorded."); } + public IGenTransaction RecordJournalEntry(IGenBase genClass, IGenBase genEntry, object? objData, object? objOldData) + { + if (genClass is null) + { + throw new ArgumentNullException(nameof(genClass)); + } + + if (genEntry is null) + { + throw new ArgumentNullException(nameof(genEntry)); + } + + var genTransaction = new GenTransaction + { + UId = Guid.NewGuid(), + Class = genClass, + Entry = genEntry, + Data = objData, + OldData = objOldData, + Timestamp = DateTime.UtcNow, + Prev = Transactions.LastOrDefault() + }; + + ((IHasOwner)genTransaction).SetOwner(this); + return RecordIncomingTransaction(genTransaction); + } + public void AttachPersistenceProvider(IGenealogyPersistenceProvider persistenceProvider) { _persistenceProvider = persistenceProvider ?? throw new ArgumentNullException(nameof(persistenceProvider)); } + private IGenTransaction RecordIncomingTransaction(IGenTransaction genTransaction) + { + if (Transactions.LastOrDefault() is GenTransaction genPreviousTransaction) + { + genPreviousTransaction.SetNext(genTransaction); + } + + Transactions.Add(genTransaction); + JournalEntryRecorded?.Invoke(this, new JournalEntryRecordedEventArgs(genTransaction)); + return genTransaction; + } + public void MarkDirty(IGenEntity? genChangedEntity = null, string? sReason = null) { _genLastChangedEntity = genChangedEntity ?? _genLastChangedEntity; From 6d8d40fe90dc6eb0ef7dbcb0ac4d4ac5d03b0cf2 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Thu, 9 Apr 2026 19:00:55 +0200 Subject: [PATCH 03/96] BaseGenClassesTests --- .../GenealogyPersistenceContextTests.cs | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/WinAhnenNew/BaseGenClassesTests/GenealogyPersistenceContextTests.cs b/WinAhnenNew/BaseGenClassesTests/GenealogyPersistenceContextTests.cs index 6e06fe73e..802174be5 100644 --- a/WinAhnenNew/BaseGenClassesTests/GenealogyPersistenceContextTests.cs +++ b/WinAhnenNew/BaseGenClassesTests/GenealogyPersistenceContextTests.cs @@ -1,9 +1,13 @@ using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; using BaseGenClasses.Model; using BaseGenClasses.Persistence; using CommunityToolkit.Mvvm.Messaging; +using GenInterfaces.Data; +using GenInterfaces.Interfaces; +using GenInterfaces.Interfaces.Genealogic; using NSubstitute; namespace BaseGenClassesTests @@ -30,5 +34,92 @@ public async Task FlushAsync_WhenGenealogyIsDirty_DelegatesToPersistenceProvider Assert.IsFalse(genGenealogy.xDirty); await prvPersistence.Received(1).FlushAsync(genGenealogy, null, GenealogyFlushScope.Auto, Arg.Any()); } + + [TestMethod] + public void Receive_WhenTransactionIsRecorded_RaisesJournalEntryRecorded() + { + var msgMessenger = new WeakReferenceMessenger(); + var genGenealogy = new Genealogy(msgMessenger) + { + UId = Guid.NewGuid() + }; + var genTransaction = Substitute.For(); + JournalEntryRecordedEventArgs? eRecordedArgs = null; + + genGenealogy.JournalEntryRecorded += (_, eArgs) => eRecordedArgs = eArgs; + + genGenealogy.Receive(genTransaction); + + Assert.IsNotNull(eRecordedArgs); + Assert.AreSame(genTransaction, eRecordedArgs.GenTransaction); + CollectionAssert.Contains(genGenealogy.JournalEntries.ToArray(), genTransaction); + } + + [TestMethod] + public void RecordSourceChange_StoresTypedSourceJournalSnapshot() + { + var msgMessenger = new WeakReferenceMessenger(); + var genGenealogy = new Genealogy(msgMessenger) + { + UId = Guid.NewGuid() + }; + var genSource = Substitute.For(); + genSource.Description.Returns("Kirchenbuch"); + genSource.Data.Returns("Taufeintrag"); + genSource.Url.Returns(new Uri("https://example.org/source")); + genSource.Medias.Returns(new System.Collections.Generic.List()); + + genGenealogy.RecordSourceChange(genSource, SourceJournalValue.FromSource(genSource), null); + + Assert.AreEqual(1, genGenealogy.JournalEntries.Count); + Assert.IsInstanceOfType(genGenealogy.JournalEntries[0].Data); + Assert.AreEqual("Kirchenbuch", ((SourceJournalValue)genGenealogy.JournalEntries[0].Data!).Description); + } + + [TestMethod] + public void RecordMediaChange_StoresTypedMediaJournalSnapshot() + { + var msgMessenger = new WeakReferenceMessenger(); + var genGenealogy = new Genealogy(msgMessenger) + { + UId = Guid.NewGuid() + }; + var genMedia = Substitute.For(); + genMedia.eMediaType.Returns(EMediaType.Picture); + genMedia.MediaName.Returns("Portrait"); + genMedia.MediaDescription.Returns("Porträtfoto"); + genMedia.MediaUri.Returns(new Uri("file:///c:/temp/portrait.jpg")); + + genGenealogy.RecordMediaChange(genMedia, MediaJournalValue.FromMedia(genMedia), null); + + Assert.AreEqual(1, genGenealogy.JournalEntries.Count); + Assert.IsInstanceOfType(genGenealogy.JournalEntries[0].Data); + Assert.AreEqual("Portrait", ((MediaJournalValue)genGenealogy.JournalEntries[0].Data!).MediaName); + } + + [TestMethod] + public void RecordRepositoryChange_StoresTypedRepositoryJournalSnapshot() + { + var msgMessenger = new WeakReferenceMessenger(); + var genGenealogy = new Genealogy(msgMessenger) + { + UId = Guid.NewGuid() + }; + var genSources = Substitute.For>(); + genSources.Count.Returns(2); + + var genRepository = Substitute.For(); + genRepository.Name.Returns("Landeskirchliches Archiv"); + genRepository.Info.Returns("Bestand A"); + genRepository.Uri.Returns(new Uri("https://example.org/archive")); + genRepository.GenSources.Returns(genSources); + + genGenealogy.RecordRepositoryChange(genRepository, RepositoryJournalValue.FromRepository(genRepository), null); + + Assert.AreEqual(1, genGenealogy.JournalEntries.Count); + Assert.IsInstanceOfType(genGenealogy.JournalEntries[0].Data); + Assert.AreEqual("Landeskirchliches Archiv", ((RepositoryJournalValue)genGenealogy.JournalEntries[0].Data!).Name); + Assert.AreEqual(2, ((RepositoryJournalValue)genGenealogy.JournalEntries[0].Data!).SourceCount); + } } } From c954a01861c129ee30043e8809778d8e8cbc7b58 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Thu, 9 Apr 2026 19:00:56 +0200 Subject: [PATCH 04/96] WinAhnenNew --- .../WinAhnenNew/Views/EditPageView.xaml | 1 + .../WinAhnenNew/Views/EditPageView.xaml.cs | 55 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/WinAhnenNew/WinAhnenNew/Views/EditPageView.xaml b/WinAhnenNew/WinAhnenNew/Views/EditPageView.xaml index 121e67d83..e7e754a6e 100644 --- a/WinAhnenNew/WinAhnenNew/Views/EditPageView.xaml +++ b/WinAhnenNew/WinAhnenNew/Views/EditPageView.xaml @@ -6,6 +6,7 @@ xmlns:local="clr-namespace:WinAhnenNew.Views" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" FontFamily="Arial" + PreviewKeyDown="Page_PreviewKeyDown" Title="EditPageView" d:DesignHeight="860" d:DesignWidth="800" diff --git a/WinAhnenNew/WinAhnenNew/Views/EditPageView.xaml.cs b/WinAhnenNew/WinAhnenNew/Views/EditPageView.xaml.cs index 8a3534468..55f177b5b 100644 --- a/WinAhnenNew/WinAhnenNew/Views/EditPageView.xaml.cs +++ b/WinAhnenNew/WinAhnenNew/Views/EditPageView.xaml.cs @@ -2,6 +2,7 @@ using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Data; +using System.Windows.Input; using System.Windows.Media; using Microsoft.Extensions.DependencyInjection; using WinAhnenNew.ViewModels; @@ -29,6 +30,44 @@ public void CommitPendingEdits() } } + private void Page_PreviewKeyDown(object sender, KeyEventArgs e) + { + if (e.Key != Key.Enter || Keyboard.Modifiers != ModifierKeys.None) + { + return; + } + + if (!TryCommitSingleLineInput(e.OriginalSource as DependencyObject)) + { + return; + } + + e.Handled = true; + } + + private static bool TryCommitSingleLineInput(DependencyObject? objSource) + { + if (objSource is null) + { + return false; + } + + var cboComboBox = FindAncestor(objSource); + if (cboComboBox is not null && cboComboBox.IsEditable) + { + UpdateBinding(cboComboBox, ComboBox.TextProperty); + return true; + } + + if (FindAncestor(objSource) is not TextBox txtTextBox || txtTextBox.AcceptsReturn) + { + return false; + } + + UpdateBinding(txtTextBox, TextBox.TextProperty); + return true; + } + private static void UpdateBindingSources(DependencyObject objRoot) { UpdateBinding(objRoot, TextBox.TextProperty); @@ -47,5 +86,21 @@ private static void UpdateBinding(DependencyObject objTarget, DependencyProperty { BindingOperations.GetBindingExpression(objTarget, dpProperty)?.UpdateSource(); } + + private static T? FindAncestor(DependencyObject? objChild) + where T : DependencyObject + { + while (objChild is not null) + { + if (objChild is T objTarget) + { + return objTarget; + } + + objChild = VisualTreeHelper.GetParent(objChild); + } + + return null; + } } } From 1cefd91a5f2e1ae37dcc0334c99e4f508d684dfe Mon Sep 17 00:00:00 2001 From: Joe Care Date: Thu, 9 Apr 2026 19:00:56 +0200 Subject: [PATCH 05/96] WinAhnenNew.Model --- .../ViewModels/EditPageViewModel.cs | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/WinAhnenNew/WinAhnenNew.Model/ViewModels/EditPageViewModel.cs b/WinAhnenNew/WinAhnenNew.Model/ViewModels/EditPageViewModel.cs index 463eb3823..37671a20f 100644 --- a/WinAhnenNew/WinAhnenNew.Model/ViewModels/EditPageViewModel.cs +++ b/WinAhnenNew/WinAhnenNew.Model/ViewModels/EditPageViewModel.cs @@ -6,6 +6,7 @@ using System.Windows.Media; using BaseGenClasses.Helper; using BaseGenClasses.Model; +using BaseGenClasses.Persistence; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Messaging; using GenInterfaces.Data; @@ -670,6 +671,7 @@ private static bool SaveSimpleFact(IGenPerson genPerson, EFactType eFactType, st { var sNormalizedValue = NormalizeValue(sValue); var genFact = genPerson.Facts.FirstOrDefault(genFact => genFact?.eFactType == eFactType); + var fctOldValue = FactJournalValue.FromFact(genFact); if (genFact is null) { @@ -678,7 +680,8 @@ private static bool SaveSimpleFact(IGenPerson genPerson, EFactType eFactType, st return false; } - genPerson.AddFact(eFactType, sNormalizedValue); + var genNewFact = genPerson.AddFact(eFactType, sNormalizedValue); + RecordFactJournalEntry(genPerson, genNewFact, null, FactJournalValue.FromFact(genNewFact)); return true; } @@ -688,6 +691,7 @@ private static bool SaveSimpleFact(IGenPerson genPerson, EFactType eFactType, st } genFact.Data = sNormalizedValue; + RecordFactJournalEntry(genPerson, genFact, fctOldValue, FactJournalValue.FromFact(genFact)); return true; } @@ -703,12 +707,14 @@ private static bool SaveEventFact( var genFact = genPerson.Facts.FirstOrDefault(genFact => genFact?.eFactType == eFactType); var dtDate = TryBuildDate(sDay, sMonth, sYear); var sNormalizedPlaceName = NormalizePlaceValue(sPlaceName); + var fctOldValue = FactJournalValue.FromFact(genFact); if (dtDate is null && string.IsNullOrWhiteSpace(sNormalizedPlaceName)) { if (genFact is not null) { genPerson.Facts.Remove(genFact); + RecordFactJournalEntry(genPerson, genFact, fctOldValue, null); return true; } @@ -737,6 +743,11 @@ private static bool SaveEventFact( xChanged = true; } + if (xChanged) + { + RecordFactJournalEntry(genPerson, genFact, fctOldValue, FactJournalValue.FromFact(genFact)); + } + return xChanged; } @@ -745,7 +756,9 @@ private static bool RemoveFact(IGenPerson genPerson, EFactType eFactType) var genFact = genPerson.Facts.FirstOrDefault(genFact => genFact?.eFactType == eFactType); if (genFact is not null) { + var fctOldValue = FactJournalValue.FromFact(genFact); genPerson.Facts.Remove(genFact); + RecordFactJournalEntry(genPerson, genFact, fctOldValue, null); return true; } @@ -784,12 +797,20 @@ private static bool RemoveFact(IGenPerson genPerson, EFactType eFactType) private static void MarkGenealogyDirty(IGenPerson genPerson, string sReason) { - if (((IHasOwner)genPerson).Owner is BaseGenClasses.Persistence.IGenealogyPersistenceContext persistenceContext) + if (((IHasOwner)genPerson).Owner is IGenealogyPersistenceContext persistenceContext) { persistenceContext.MarkDirty(genPerson, sReason); } } + private static void RecordFactJournalEntry(IGenPerson genPerson, IGenFact genFact, FactJournalValue? fctOldValue, FactJournalValue? fctNewValue) + { + if (((IHasOwner)genPerson).Owner is IGenealogyJournalContext journalContext) + { + journalContext.RecordJournalEntry(genPerson, genFact, fctNewValue, fctOldValue); + } + } + private static bool AreEquivalentDates(IGenDate? genLeftDate, IGenDate? genRightDate) { if (ReferenceEquals(genLeftDate, genRightDate)) From 16936cce4b86209fde61451d8b09e6fb6d57095f Mon Sep 17 00:00:00 2001 From: Joe Care Date: Thu, 9 Apr 2026 19:00:56 +0200 Subject: [PATCH 06/96] WinAhnenNew.Model.Tests --- .../WinAhnenNew.Model.Tests/EditPageViewModelTests.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/WinAhnenNew/WinAhnenNew.Model.Tests/EditPageViewModelTests.cs b/WinAhnenNew/WinAhnenNew.Model.Tests/EditPageViewModelTests.cs index 7afb43052..260f92ddf 100644 --- a/WinAhnenNew/WinAhnenNew.Model.Tests/EditPageViewModelTests.cs +++ b/WinAhnenNew/WinAhnenNew.Model.Tests/EditPageViewModelTests.cs @@ -4,6 +4,7 @@ using BaseGenClasses.Helper.Interfaces; using CommunityToolkit.Mvvm.Messaging; using BaseGenClasses.Model; +using BaseGenClasses.Persistence; using BaseLib.Helper; using GenInterfaces.Data; using GenInterfaces.Interfaces; @@ -44,6 +45,12 @@ public void LastNameChange_UpdatesUnderlyingSelectedPersonWithoutPersistingImmed vmEdit.LastName = "Schulze"; Assert.AreEqual("Schulze", genPerson.Surname); + Assert.IsTrue(genGenealogy.xDirty); + Assert.AreEqual(1, genGenealogy.JournalEntries.Count); + Assert.AreEqual(genPerson, genGenealogy.JournalEntries[0].Class); + Assert.AreEqual(EFactType.Surname, ((FactJournalValue)genGenealogy.JournalEntries[0].OldData!).eFactType); + Assert.AreEqual("Meyer", ((FactJournalValue)genGenealogy.JournalEntries[0].OldData!).Data); + Assert.AreEqual("Schulze", ((FactJournalValue)genGenealogy.JournalEntries[0].Data!).Data); svcPersonSelection.DidNotReceive().SaveChanges(); } From 34faa165a88ac61bd505a5be7115b3e6c9575cbf Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 00:31:56 +0200 Subject: [PATCH 07/96] SharpHack.Server --- .../Libraries/MVVM_BaseLibTests/MVVM_BaseLibTests.csproj | 2 +- CSharpBible/MVVM_Tutorial/WpfApp1/WpfApp_net.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CSharpBible/Libraries/MVVM_BaseLibTests/MVVM_BaseLibTests.csproj b/CSharpBible/Libraries/MVVM_BaseLibTests/MVVM_BaseLibTests.csproj index 45dd12f50..c19b5744c 100644 --- a/CSharpBible/Libraries/MVVM_BaseLibTests/MVVM_BaseLibTests.csproj +++ b/CSharpBible/Libraries/MVVM_BaseLibTests/MVVM_BaseLibTests.csproj @@ -18,7 +18,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/WpfApp1/WpfApp_net.csproj b/CSharpBible/MVVM_Tutorial/WpfApp1/WpfApp_net.csproj index 40e6f148e..87f3d9812 100644 --- a/CSharpBible/MVVM_Tutorial/WpfApp1/WpfApp_net.csproj +++ b/CSharpBible/MVVM_Tutorial/WpfApp1/WpfApp_net.csproj @@ -27,7 +27,7 @@ - + From 519b8ebf32078dd976d6d4767b2d41c5a864b949 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 00:36:11 +0200 Subject: [PATCH 08/96] DAO --- .../Persistence/SourceJournalValue.cs | 51 ++ .../AmtsblattLoader.Console.csproj | 18 + .../AmtsblattLoader.Console/Program.cs | 17 + .../AmtsblattLoaderConsoleViewModel.cs | 40 ++ .../Views/ConsoleOutputView.cs | 23 + WinAhnenNew/RnzTrauer/Directory.Build.props | 7 + .../RnzTrauer/Directory.Packages.props | 14 + .../RnzTrauer/RnzTrauer.Console/Program.cs | 17 + .../RnzTrauer.Console.csproj | 24 + .../ViewModels/RnzTrauerConsoleViewModel.cs | 323 ++++++++++ .../Views/ConsoleOutputView.cs | 39 ++ .../RnzTrauer.Core/Models/AmtsblattConfig.cs | 30 + .../RnzTrauer.Core/Models/DatabaseSettings.cs | 27 + .../RnzTrauer.Core/Models/RnzConfig.cs | 40 ++ .../RnzTrauer.Core/Models/WebQuery.cs | 34 ++ .../RnzTrauer.Core/RnzTrauer.Core.csproj | 15 + .../Services/AmtsblattWebHandler.cs | 115 ++++ .../RnzTrauer.Core/Services/ConfigLoader.cs | 29 + .../RnzTrauer.Core/Services/DataHandler.cs | 550 ++++++++++++++++++ .../RnzTrauer.Core/Services/PortedHelpers.cs | 288 +++++++++ .../RnzTrauer.Core/Services/WebHandler.cs | 530 +++++++++++++++++ .../RnzTrauer.Tests/PortedHelpersTests.cs | 62 ++ .../RnzTrauer.Tests/RnzTrauer.Tests.csproj | 23 + 23 files changed, 2316 insertions(+) create mode 100644 WinAhnenNew/BaseGenClasses/Persistence/SourceJournalValue.cs create mode 100644 WinAhnenNew/RnzTrauer/AmtsblattLoader.Console/AmtsblattLoader.Console.csproj create mode 100644 WinAhnenNew/RnzTrauer/AmtsblattLoader.Console/Program.cs create mode 100644 WinAhnenNew/RnzTrauer/AmtsblattLoader.Console/ViewModels/AmtsblattLoaderConsoleViewModel.cs create mode 100644 WinAhnenNew/RnzTrauer/AmtsblattLoader.Console/Views/ConsoleOutputView.cs create mode 100644 WinAhnenNew/RnzTrauer/Directory.Build.props create mode 100644 WinAhnenNew/RnzTrauer/Directory.Packages.props create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Console/Program.cs create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Console/RnzTrauer.Console.csproj create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Console/ViewModels/RnzTrauerConsoleViewModel.cs create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Console/Views/ConsoleOutputView.cs create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Core/Models/AmtsblattConfig.cs create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Core/Models/DatabaseSettings.cs create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Core/Models/RnzConfig.cs create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Core/Models/WebQuery.cs create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Core/RnzTrauer.Core.csproj create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/AmtsblattWebHandler.cs create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/ConfigLoader.cs create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/DataHandler.cs create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/PortedHelpers.cs create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/WebHandler.cs create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Tests/PortedHelpersTests.cs create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Tests/RnzTrauer.Tests.csproj diff --git a/WinAhnenNew/BaseGenClasses/Persistence/SourceJournalValue.cs b/WinAhnenNew/BaseGenClasses/Persistence/SourceJournalValue.cs new file mode 100644 index 000000000..1b94dfbdc --- /dev/null +++ b/WinAhnenNew/BaseGenClasses/Persistence/SourceJournalValue.cs @@ -0,0 +1,51 @@ +using System; +using GenInterfaces.Interfaces.Genealogic; + +namespace BaseGenClasses.Persistence; + +/// +/// Represents a serializable source snapshot for genealogy journal entries. +/// +public sealed class SourceJournalValue +{ + /// + /// Gets or sets the short description of the source. + /// + public string? Description { get; init; } + + /// + /// Gets or sets the source URI. + /// + public string? Url { get; init; } + + /// + /// Gets or sets the source payload text. + /// + public string? Data { get; init; } + + /// + /// Gets or sets the number of linked media items. + /// + public int MediaCount { get; init; } + + /// + /// Creates a journal snapshot from a genealogy source. + /// + /// The source to snapshot. + /// The snapshot value, or when no source is available. + public static SourceJournalValue? FromSource(IGenSource? genSource) + { + if (genSource is null) + { + return null; + } + + return new SourceJournalValue + { + Description = genSource.Description, + Url = genSource.Url?.ToString(), + Data = genSource.Data, + MediaCount = genSource.Medias.Count + }; + } +} diff --git a/WinAhnenNew/RnzTrauer/AmtsblattLoader.Console/AmtsblattLoader.Console.csproj b/WinAhnenNew/RnzTrauer/AmtsblattLoader.Console/AmtsblattLoader.Console.csproj new file mode 100644 index 000000000..81f4a1607 --- /dev/null +++ b/WinAhnenNew/RnzTrauer/AmtsblattLoader.Console/AmtsblattLoader.Console.csproj @@ -0,0 +1,18 @@ + + + + + + + + + + + + Exe + net10.0 + enable + enable + + + diff --git a/WinAhnenNew/RnzTrauer/AmtsblattLoader.Console/Program.cs b/WinAhnenNew/RnzTrauer/AmtsblattLoader.Console/Program.cs new file mode 100644 index 000000000..a0d35639d --- /dev/null +++ b/WinAhnenNew/RnzTrauer/AmtsblattLoader.Console/Program.cs @@ -0,0 +1,17 @@ +using AmtsblattLoader.Console.ViewModels; +using AmtsblattLoader.Console.Views; +using RnzTrauer.Core; + +var xView = new ConsoleOutputView(); + +try +{ + var xConfig = AmtsblattConfig.Load(Path.Combine(AppContext.BaseDirectory, "Amtsblatt_Cfg.json")); + var xViewModel = new AmtsblattLoaderConsoleViewModel(xView); + xViewModel.Run(xConfig); +} +catch (FileNotFoundException ex) +{ + xView.WriteErrorLine(ex.Message); + xView.WriteErrorLine("Lege eine Datei `Amtsblatt_Cfg.json` neben die EXE. Eine Vorlage liegt als `Amtsblatt_Cfg.sample.json` im Projekt."); +} diff --git a/WinAhnenNew/RnzTrauer/AmtsblattLoader.Console/ViewModels/AmtsblattLoaderConsoleViewModel.cs b/WinAhnenNew/RnzTrauer/AmtsblattLoader.Console/ViewModels/AmtsblattLoaderConsoleViewModel.cs new file mode 100644 index 000000000..4f5ab16d4 --- /dev/null +++ b/WinAhnenNew/RnzTrauer/AmtsblattLoader.Console/ViewModels/AmtsblattLoaderConsoleViewModel.cs @@ -0,0 +1,40 @@ +using AmtsblattLoader.Console.Views; +using RnzTrauer.Core; + +namespace AmtsblattLoader.Console.ViewModels; + +/// +/// Coordinates the Amtsblatt console workflow. +/// +public sealed class AmtsblattLoaderConsoleViewModel +{ + private readonly ConsoleOutputView _view; + + /// + /// Initializes a new instance of the class. + /// + public AmtsblattLoaderConsoleViewModel(ConsoleOutputView xView) + { + _view = xView; + } + + /// + /// Runs the Amtsblatt loader workflow. + /// + public void Run(AmtsblattConfig xConfig) + { + using var xWebHandler = new AmtsblattWebHandler(xConfig); + xWebHandler.InitPage(); + var iOffset = 1; + var iDayDelta = 0; + while (iDayDelta <= 400) + { + var dtCurrent = DateOnly.FromDateTime(DateTime.Today).AddDays(-(iDayDelta + iOffset)); + iDayDelta += 7; + var iWeek = (dtCurrent.DayOfYear - 1) / 7; + var sStart = $"{xConfig.Url}-{iWeek:00}-{dtCurrent.Year:0000}"; + _view.WriteLine($"Load: {sStart}"); + xWebHandler.GetData1(sStart); + } + } +} diff --git a/WinAhnenNew/RnzTrauer/AmtsblattLoader.Console/Views/ConsoleOutputView.cs b/WinAhnenNew/RnzTrauer/AmtsblattLoader.Console/Views/ConsoleOutputView.cs new file mode 100644 index 000000000..0dae32466 --- /dev/null +++ b/WinAhnenNew/RnzTrauer/AmtsblattLoader.Console/Views/ConsoleOutputView.cs @@ -0,0 +1,23 @@ +namespace AmtsblattLoader.Console.Views; + +/// +/// Provides console-based output for the Amtsblatt console application. +/// +public sealed class ConsoleOutputView +{ + /// + /// Writes text followed by a newline. + /// + public void WriteLine(string sText = "") + { + System.Console.WriteLine(sText); + } + + /// + /// Writes an error line. + /// + public void WriteErrorLine(string sText) + { + System.Console.Error.WriteLine(sText); + } +} diff --git a/WinAhnenNew/RnzTrauer/Directory.Build.props b/WinAhnenNew/RnzTrauer/Directory.Build.props new file mode 100644 index 000000000..e7cd6b498 --- /dev/null +++ b/WinAhnenNew/RnzTrauer/Directory.Build.props @@ -0,0 +1,7 @@ + + + $(MSBuildThisFileDirectory)..\..\bin\$(MSBuildProjectName)\ + $(MSBuildThisFileDirectory)..\..\obj\$(MSBuildProjectName)\ + $(DefaultItemExcludes);$(MSBuildProjectDirectory)\bin\**;$(MSBuildProjectDirectory)\obj\** + + diff --git a/WinAhnenNew/RnzTrauer/Directory.Packages.props b/WinAhnenNew/RnzTrauer/Directory.Packages.props new file mode 100644 index 000000000..3af7aeb4f --- /dev/null +++ b/WinAhnenNew/RnzTrauer/Directory.Packages.props @@ -0,0 +1,14 @@ + + + true + + + + + + + + + + + \ No newline at end of file diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Console/Program.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Console/Program.cs new file mode 100644 index 000000000..1edeba79c --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Console/Program.cs @@ -0,0 +1,17 @@ +using RnzTrauer.Console.ViewModels; +using RnzTrauer.Console.Views; +using RnzTrauer.Core; + +var xView = new ConsoleOutputView(); + +try +{ + var xConfig = RnzConfig.Load(Path.Combine(AppContext.BaseDirectory, "RNZ_Config.json")); + var xViewModel = new RnzTrauerConsoleViewModel(xView); + xViewModel.Run(xConfig); +} +catch (FileNotFoundException ex) +{ + xView.WriteErrorLine(ex.Message); + xView.WriteErrorLine("Lege eine Datei `RNZ_Config.json` neben die EXE. Eine Vorlage liegt als `RNZ_Config.sample.json` im Projekt."); +} diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Console/RnzTrauer.Console.csproj b/WinAhnenNew/RnzTrauer/RnzTrauer.Console/RnzTrauer.Console.csproj new file mode 100644 index 000000000..4d005f321 --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Console/RnzTrauer.Console.csproj @@ -0,0 +1,24 @@ + + + + + + + + + + + + + PreserveNewest + + + + + Exe + net10.0 + enable + enable + + + diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Console/ViewModels/RnzTrauerConsoleViewModel.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Console/ViewModels/RnzTrauerConsoleViewModel.cs new file mode 100644 index 000000000..def394184 --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Console/ViewModels/RnzTrauerConsoleViewModel.cs @@ -0,0 +1,323 @@ +using System.Text; +using System.Text.Json.Nodes; +using RnzTrauer.Console.Views; +using RnzTrauer.Core; + +namespace RnzTrauer.Console.ViewModels; + +/// +/// Coordinates the RNZ console workflow while keeping user-facing output outside the core services. +/// +public sealed class RnzTrauerConsoleViewModel +{ + private readonly ConsoleOutputView _view; + + /// + /// Initializes a new instance of the class. + /// + public RnzTrauerConsoleViewModel(ConsoleOutputView xView) + { + _view = xView; + } + + /// + /// Runs the RNZ scraping and import workflow. + /// + public void Run(RnzConfig xConfig) + { + _view.WriteLine("Start..."); + using var xWebHandler = new WebHandler(xConfig); + xWebHandler.InitPage(); + + _view.WriteLine("Init..."); + var iOffset = 35; + var iDayDelta = 0; + while (iDayDelta <= 800) + { + var dtCurrent = DateOnly.FromDateTime(DateTime.Today).AddDays(-(iDayDelta + iOffset)); + iDayDelta += 1; + var sStart = $"https://trauer.rnz.de/traueranzeigen-suche/erscheinungstag-{dtCurrent.Day:00}-{dtCurrent.Month:00}-{dtCurrent.Year:0000}"; + var (dPages, arrItems) = xWebHandler.GetData1(sStart); + + _view.Write("Compute "); + for (var iIndex = 0; iIndex < arrItems.Count; iIndex++) + { + var dEntry = new Dictionary(arrItems[iIndex], StringComparer.Ordinal); + try + { + if (dEntry.TryGetValue(WebHandler.CsData, out var xDataObject) && xDataObject is byte[] arrData && arrData.Length >= 10) + { + var sPrefix = Encoding.ASCII.GetString(arrData.Take(10).ToArray()); + if (sPrefix.Contains("PDF", StringComparison.Ordinal)) + { + dEntry["pdfText"] = PortedHelpers.PdfText(arrData); + arrItems[iIndex] = dEntry; + if (dEntry.TryGetValue("parent", out var xParentObject)) + { + var sParent = Convert.ToString(xParentObject) ?? string.Empty; + if (dPages.TryGetValue(sParent, out var dParentPage) && dEntry.TryGetValue(WebHandler.CsSrc, out var xSourceObject)) + { + dParentPage[Convert.ToString(xSourceObject) ?? string.Empty] = new Dictionary + { + ["pdfText"] = dEntry["pdfText"] + }; + } + } + + _view.Write('+'); + } + else + { + _view.Write('.'); + } + } + } + catch + { + } + } + + _view.Write("\nSave "); + SavePages(xConfig, dPages); + _view.Write("\nSave Media:"); + SaveMedia(xConfig, arrItems, dtCurrent); + _view.WriteLine(); + } + + xWebHandler.Close(); + + using var xDataHandler = new DataHandler(xConfig); + iDayDelta = 0; + while (iDayDelta <= 14) + { + var dtCurrent = DateOnly.FromDateTime(DateTime.Today).AddDays(-(iDayDelta + iOffset)); + _view.WriteLine($"Handle: {dtCurrent}"); + iDayDelta += 1; + foreach (var sAnnouncementType in new[] { "todesanzeigen", "nachrufe", "danksagungen", "_" }) + { + _view.WriteLine($"Type: {sAnnouncementType}"); + var iPage = 0; + while (iPage < 20) + { + iPage += 1; + _view.WriteLine($"Page: {iPage}"); + var sStart = iPage == 1 + ? $"https://trauer.rnz.de/traueranzeigen-suche/erscheinungstag-{dtCurrent.Day:00}-{dtCurrent.Month:00}-{dtCurrent.Year:0000}/anzeigenart-{sAnnouncementType}" + : $"https://trauer.rnz.de/traueranzeigen-suche/erscheinungstag-{dtCurrent.Day:00}-{dtCurrent.Month:00}-{dtCurrent.Year:0000}/anzeigenart-{sAnnouncementType}/seite-{iPage}"; + var sPath = PortedHelpers.GetLocalPath(sStart, xConfig.LocalPath, dtCurrent); + var sJsonFile = Path.HasExtension(sPath) + ? Path.ChangeExtension(sPath, ".json") + : sPath + ".json"; + if (File.Exists(sJsonFile)) + { + var xData = JsonNode.Parse(File.ReadAllText(sJsonFile)); + var arrTrauerfaelle = DataHandler.ExtractTrauerData(xData, xConfig.LocalPath); + xDataHandler.TrauerDataToDb(arrTrauerfaelle, xConfig.LocalPath); + } + else + { + if (iPage == 1) + { + _view.Write('-'); + } + + iPage = 99; + } + } + } + } + } + + private void SavePages(RnzConfig xConfig, Dictionary> dPages) + { + foreach (var dPage in dPages.Values) + { + var dParentData = new Dictionary(StringComparer.Ordinal); + var dParentPaths = new Dictionary(StringComparer.Ordinal); + var xParentChanged = false; + + if (dPage.TryGetValue("parent", out var xParentObject) && xParentObject is List arrParents) + { + foreach (var xParentValue in arrParents) + { + var sParent = Convert.ToString(xParentValue) ?? string.Empty; + var sParentPath = PortedHelpers.GetLocalPath(sParent, xConfig.LocalPath); + sParentPath = Path.HasExtension(sParentPath) ? Path.ChangeExtension(sParentPath, ".json") : sParentPath + ".json"; + if (File.Exists(sParentPath)) + { + try + { + dParentData[sParent] = JsonNode.Parse(File.ReadAllText(sParentPath)) as JsonObject ?? new JsonObject(); + } + catch + { + dParentData[sParent] = new JsonObject(); + xParentChanged = true; + } + } + else + { + dParentData[sParent] = new JsonObject(); + xParentChanged = true; + } + + dParentPaths[sParent] = sParentPath; + } + } + + var sLocalPath = PortedHelpers.GetLocalPath(Convert.ToString(dPage["url"]) ?? string.Empty, xConfig.LocalPath); + var sHtmlPath = Path.HasExtension(sLocalPath) ? sLocalPath : sLocalPath + ".html"; + Directory.CreateDirectory(Path.GetDirectoryName(sHtmlPath)!); + File.WriteAllText(sHtmlPath, Convert.ToString(dPage["content"]) ?? string.Empty); + + var sJsonPath = Path.ChangeExtension(sHtmlPath, ".json"); + var xPageNode = PortedHelpers.ToJsonObject(dPage); + if (File.Exists(sJsonPath)) + { + try + { + var xOldNode = JsonNode.Parse(File.ReadAllText(sJsonPath)) as JsonObject; + if (xOldNode is not null) + { + foreach (var kvValue in xOldNode) + { + if (kvValue.Key.StartsWith("http", StringComparison.Ordinal) && !xPageNode.ContainsKey(kvValue.Key)) + { + xPageNode[kvValue.Key] = kvValue.Value?.DeepClone(); + } + } + } + } + catch + { + _view.Write('-'); + } + } + + File.WriteAllText(sJsonPath, xPageNode.ToJsonString(PortedHelpers.JsonOptions)); + + if (dPage.TryGetValue("parent", out xParentObject) && xParentObject is List arrParentList) + { + foreach (var xParentValue in arrParentList) + { + var sParent = Convert.ToString(xParentValue) ?? string.Empty; + var dEntryCopy = new Dictionary(dPage, StringComparer.Ordinal); + dEntryCopy.Remove("content"); + dEntryCopy["localpath"] = sJsonPath; + var sUrl = Convert.ToString(dPage["url"]) ?? string.Empty; + if (!dParentData[sParent].ContainsKey(sUrl)) + { + dParentData[sParent][sUrl] = PortedHelpers.ToJsonObject(dEntryCopy); + xParentChanged = true; + } + else if (dParentData[sParent][sUrl] is JsonObject xTarget) + { + foreach (var kvValue in dEntryCopy) + { + xTarget[kvValue.Key] = PortedHelpers.ToJsonNode(kvValue.Value); + } + + xParentChanged = true; + } + + if (xParentChanged) + { + Directory.CreateDirectory(Path.GetDirectoryName(dParentPaths[sParent])!); + File.WriteAllText(dParentPaths[sParent], dParentData[sParent].ToJsonString(PortedHelpers.JsonOptions)); + } + } + } + + _view.Write('.'); + } + } + + private void SaveMedia(RnzConfig xConfig, List> arrItems, DateOnly dtCurrent) + { + foreach (var dEntry in arrItems) + { + try + { + var dEntryCopy = new Dictionary(dEntry, StringComparer.Ordinal); + dEntryCopy.Remove(WebHandler.CsData); + var sParent = Convert.ToString(dEntry.GetValueOrDefault("parent")) ?? string.Empty; + var sParentPath = PortedHelpers.GetLocalPath(sParent, xConfig.LocalPath); + sParentPath = Path.HasExtension(sParentPath) ? Path.ChangeExtension(sParentPath, ".json") : sParentPath + ".json"; + JsonObject xParentData; + var xParentChanged = false; + if (File.Exists(sParentPath)) + { + try + { + xParentData = JsonNode.Parse(File.ReadAllText(sParentPath)) as JsonObject ?? new JsonObject(); + } + catch + { + xParentData = new JsonObject(); + xParentChanged = true; + } + } + else + { + xParentData = new JsonObject(); + xParentChanged = true; + } + + if (dEntryCopy.TryGetValue("Header", out var xHeaderObject) && xHeaderObject is Dictionary dHeaders) + { + dEntryCopy["Header"] = dHeaders.ToDictionary(k => k.Key, v => (object?)v.Value, StringComparer.OrdinalIgnoreCase); + } + + if (dEntry.TryGetValue("href", out var xHrefObject)) + { + var sHref = Convert.ToString(xHrefObject) ?? string.Empty; + var sLocalPath = PortedHelpers.GetLocalPath(sHref, xConfig.LocalPath, dtCurrent); + var sDataPath = Path.Combine(sLocalPath, $"data_{dtCurrent:yyyy-MM-dd}.json"); + Directory.CreateDirectory(Path.GetDirectoryName(sDataPath)!); + File.WriteAllText(sDataPath, PortedHelpers.ToJsonObject(dEntryCopy).ToJsonString(PortedHelpers.JsonOptions)); + } + + var sSource = Convert.ToString(dEntry.GetValueOrDefault(WebHandler.CsSrc)) ?? string.Empty; + if (!string.IsNullOrEmpty(sSource) && dEntry.TryGetValue(WebHandler.CsData, out var xDataObject) && xDataObject is byte[] arrData) + { + var sLocalPath = PortedHelpers.GetLocalPath(sSource, xConfig.LocalPath); + var sFilePath = sLocalPath; + var sPrefix = Encoding.ASCII.GetString(arrData.Take(10).ToArray()); + if (sPrefix.Contains("PNG", StringComparison.Ordinal)) + { + sFilePath = Path.ChangeExtension(sFilePath, ".png"); + } + else if (sPrefix.Contains("PDF", StringComparison.Ordinal)) + { + sFilePath = Path.ChangeExtension(sFilePath, ".pdf"); + } + else if (sPrefix.Contains("JFIF", StringComparison.Ordinal)) + { + sFilePath = Path.ChangeExtension(sFilePath, ".jpeg"); + } + else if (sPrefix.Contains(" +/// Provides console-based output for the RNZ console application. +/// +public sealed class ConsoleOutputView +{ + /// + /// Writes text without a trailing newline. + /// + public void Write(string sText) + { + System.Console.Write(sText); + } + + /// + /// Writes a single character without a trailing newline. + /// + public void Write(char cValue) + { + System.Console.Write(cValue); + } + + /// + /// Writes text followed by a newline. + /// + public void WriteLine(string sText = "") + { + System.Console.WriteLine(sText); + } + + /// + /// Writes an error line. + /// + public void WriteErrorLine(string sText) + { + System.Console.Error.WriteLine(sText); + } +} diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Models/AmtsblattConfig.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Models/AmtsblattConfig.cs new file mode 100644 index 000000000..af1064011 --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Models/AmtsblattConfig.cs @@ -0,0 +1,30 @@ +namespace RnzTrauer.Core; + +/// +/// Describes the Amtsblatt loader configuration loaded from JSON. +/// +public sealed class AmtsblattConfig : DatabaseSettings +{ + /// + /// Gets or sets the page URL prefix. + /// + public string Url { get; set; } = string.Empty; + + /// + /// Gets or sets the expected page title. + /// + public string Title { get; set; } = string.Empty; + + /// + /// Gets or sets the local storage root. + /// + public string LocalPath { get; set; } = string.Empty; + + /// + /// Loads the configuration from a JSON file. + /// + public static AmtsblattConfig Load(string sFilePath) + { + return ConfigLoader.Load(sFilePath); + } +} diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Models/DatabaseSettings.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Models/DatabaseSettings.cs new file mode 100644 index 000000000..76d5364f5 --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Models/DatabaseSettings.cs @@ -0,0 +1,27 @@ +namespace RnzTrauer.Core; + +/// +/// Provides database connection settings shared by the ported applications. +/// +public class DatabaseSettings +{ + /// + /// Gets or sets the database user name. + /// + public string DBuser { get; set; } = string.Empty; + + /// + /// Gets or sets the database password. + /// + public string DBpass { get; set; } = string.Empty; + + /// + /// Gets or sets the database host name. + /// + public string DBhost { get; set; } = string.Empty; + + /// + /// Gets or sets the database name. + /// + public string DB { get; set; } = string.Empty; +} diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Models/RnzConfig.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Models/RnzConfig.cs new file mode 100644 index 000000000..a84fa0cae --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Models/RnzConfig.cs @@ -0,0 +1,40 @@ +namespace RnzTrauer.Core; + +/// +/// Describes the RNZ scraper configuration loaded from JSON. +/// +public sealed class RnzConfig : DatabaseSettings +{ + /// + /// Gets or sets the login URL. + /// + public string Url { get; set; } = string.Empty; + + /// + /// Gets or sets the expected page title after navigation. + /// + public string Title { get; set; } = string.Empty; + + /// + /// Gets or sets the login user name. + /// + public string User { get; set; } = string.Empty; + + /// + /// Gets or sets the login password. + /// + public string Password { get; set; } = string.Empty; + + /// + /// Gets or sets the local storage root. + /// + public string LocalPath { get; set; } = string.Empty; + + /// + /// Loads the configuration from a JSON file. + /// + public static RnzConfig Load(string sFilePath) + { + return ConfigLoader.Load(sFilePath); + } +} diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Models/WebQuery.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Models/WebQuery.cs new file mode 100644 index 000000000..6d3e97412 --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Models/WebQuery.cs @@ -0,0 +1,34 @@ +using OpenQA.Selenium; + +namespace RnzTrauer.Core; + +/// +/// Describes a recursive Selenium query used to capture HTML fragments into dictionaries. +/// +public sealed class WebQuery +{ + /// + /// Initializes a new instance of the class. + /// + public WebQuery(string sName, By bySelector, params WebQuery[] arrChildren) + { + Name = sName; + By = bySelector; + Children = arrChildren; + } + + /// + /// Gets the target property name. + /// + public string Name { get; } + + /// + /// Gets the Selenium selector. + /// + public By By { get; } + + /// + /// Gets the nested child queries. + /// + public IReadOnlyList Children { get; } +} diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/RnzTrauer.Core.csproj b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/RnzTrauer.Core.csproj new file mode 100644 index 000000000..545f1d64c --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/RnzTrauer.Core.csproj @@ -0,0 +1,15 @@ + + + + net10.0 + enable + enable + + + + + + + + + diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/AmtsblattWebHandler.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/AmtsblattWebHandler.cs new file mode 100644 index 000000000..284b9f224 --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/AmtsblattWebHandler.cs @@ -0,0 +1,115 @@ +using OpenQA.Selenium; +using OpenQA.Selenium.Firefox; + +namespace RnzTrauer.Core; + +/// +/// Provides Selenium-based scraping for the Amtsblatt loader. +/// +public sealed class AmtsblattWebHandler : IDisposable +{ + private readonly AmtsblattConfig _config; + + /// + /// Initializes a new instance of the class. + /// + public AmtsblattWebHandler(AmtsblattConfig xConfig) + { + _config = xConfig; + } + + /// + /// Gets the active Firefox driver instance. + /// + public FirefoxDriver? Driver { get; private set; } + + /// + /// Opens the configured start page. + /// + public void InitPage() + { + var xOptions = new FirefoxOptions(); + Driver = new FirefoxDriver(xOptions); + Driver.Navigate().GoToUrl(_config.Url); + while (Driver.Title != _config.Title) + { + Thread.Sleep(500); + } + + Thread.Sleep(500); + } + + /// + /// Loads the target issue page and triggers the PDF download action. + /// + public (Dictionary> Pages, List> Items) GetData1(string sStart) + { + var xDriver = Driver ?? throw new InvalidOperationException("The web driver has not been initialized."); + xDriver.Navigate().GoToUrl(sStart); + while (xDriver.Title == "RNZ" || string.IsNullOrEmpty(xDriver.Title)) + { + Thread.Sleep(500); + } + + var dPages = new Dictionary>(StringComparer.Ordinal); + var arrItems = new List>(); + var sNextUrl = "."; + var iCounter = 0; + while (!string.IsNullOrEmpty(sNextUrl) && iCounter < 20) + { + Console.WriteLine(xDriver.Url); + (_, _, sNextUrl) = WorkMainPage(dPages, arrItems); + iCounter++; + } + + return (dPages, arrItems); + } + + /// + /// Closes the browser session. + /// + public void Close() + { + Driver?.Quit(); + Driver = null; + } + + /// + public void Dispose() + { + Close(); + } + + private (List SubPages, string Url, string NextUrl) WorkMainPage(Dictionary> dPages, List> arrItems) + { + var xDriver = Driver ?? throw new InvalidOperationException("The web driver has not been initialized."); + var sUrl = xDriver.Url; + var dPage = new Dictionary + { + ["Title"] = xDriver.Title, + ["url"] = sUrl, + ["sections"] = new List>(), + ["content"] = xDriver.PageSource + }; + dPages[sUrl] = dPage; + + Thread.Sleep(4000); + foreach (var xButton in xDriver.FindElements(By.TagName("button"))) + { + if ((xButton.Text ?? string.Empty).StartsWith("Alle akz", StringComparison.Ordinal)) + { + xButton.Click(); + } + } + + foreach (var xLink in xDriver.FindElements(By.TagName("a"))) + { + if ((xLink.Text ?? string.Empty).StartsWith("PDF her", StringComparison.Ordinal)) + { + xLink.Click(); + } + } + + return ([], sUrl, string.Empty); + } +} diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/ConfigLoader.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/ConfigLoader.cs new file mode 100644 index 000000000..c86ced232 --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/ConfigLoader.cs @@ -0,0 +1,29 @@ +using System.Text.Json; + +namespace RnzTrauer.Core; + +/// +/// Provides JSON-based configuration loading for the ported tools. +/// +public static class ConfigLoader +{ + private static readonly JsonSerializerOptions _options = new() + { + PropertyNameCaseInsensitive = true, + WriteIndented = true + }; + + /// + /// Loads a configuration instance from the specified JSON file. + /// + public static T Load(string sFilePath) where T : new() + { + if (!File.Exists(sFilePath)) + { + throw new FileNotFoundException($"Configuration file was not found: {sFilePath}"); + } + + var xConfiguration = JsonSerializer.Deserialize(File.ReadAllText(sFilePath), _options); + return xConfiguration ?? new T(); + } +} diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/DataHandler.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/DataHandler.cs new file mode 100644 index 000000000..315a08274 --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/DataHandler.cs @@ -0,0 +1,550 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Nodes; +using MySqlConnector; + +namespace RnzTrauer.Core; + +/// +/// Provides database access and extraction helpers for the RNZ obituary data. +/// +public sealed class DataHandler : IDisposable +{ + private readonly MySqlConnection _dbConn; + + /// + /// Initializes a new instance of the class. + /// + public DataHandler(DatabaseSettings xSettings) + { + var sConnectionString = new MySqlConnectionStringBuilder + { + Server = xSettings.DBhost, + Port = 3306, + UserID = xSettings.DBuser, + Password = xSettings.DBpass, + Database = xSettings.DB, + AllowUserVariables = true + }.ConnectionString; + + _dbConn = new MySqlConnection(sConnectionString); + _dbConn.Open(); + } + + /// + /// Gets the in-memory obituary case index. + /// + public Dictionary TfIdx { get; private set; } = new(StringComparer.Ordinal); + + /// + /// Extracts obituary dictionaries from the stored page JSON structure. + /// + public static List> ExtractTrauerData(JsonNode? xData, string sLocalPathRoot) + { + var arrResult = new List>(); + if (xData is not JsonObject xRoot || xRoot["sections"] is not JsonArray arrSections) + { + return arrResult; + } + + foreach (var xSectionNode in arrSections.OfType()) + { + var arrTexts = xSectionNode["text"] as JsonArray; + if (arrTexts is null || arrTexts.Count == 0 || !(arrTexts[0]?.ToString() ?? string.Empty).StartsWith("ANZ", StringComparison.Ordinal)) + { + continue; + } + + JsonObject xTrauerfallData = new(); + if (xSectionNode["links"] is JsonArray arrLinks && arrLinks.Count > 0 && arrLinks[0] is JsonObject xLink0) + { + var sLinkHref = xLink0["href"]?.ToString() ?? string.Empty; + if (xRoot[$"{sLinkHref}/anzeigen"] is JsonObject xInline) + { + xTrauerfallData = xInline; + Console.Write('.'); + } + else + { + var xFileInfo = new FileInfo(PortedHelpers.GetLocalPath(sLinkHref, sLocalPathRoot)); + var sFullName = xFileInfo.Extension.Length == 0 ? Path.Combine(xFileInfo.DirectoryName ?? string.Empty, xFileInfo.Name + ".json") : xFileInfo.FullName; + if (File.Exists(sFullName)) + { + xTrauerfallData = JsonNode.Parse(File.ReadAllText(sFullName)) as JsonObject ?? new JsonObject(); + Console.Write(','); + } + else + { + Console.Write('-'); + } + } + } + + if (xTrauerfallData["sections"] is not JsonArray arrTrauerfallSections) + { + continue; + } + + var sProfileImagePath = string.Empty; + foreach (var xAnnouncementNode in arrTrauerfallSections.OfType()) + { + var sCssClass = xAnnouncementNode["class"]?.ToString() ?? string.Empty; + if (sCssClass.StartsWith("col-12", StringComparison.Ordinal)) + { + try + { + if (xAnnouncementNode["imgs"] is JsonArray arrImages && arrImages.Count > 0 && arrImages[0] is JsonObject xImage0) + { + var sSource = xImage0["src"]?.ToString() ?? string.Empty; + if (sSource.Contains("MEDIA", StringComparison.Ordinal)) + { + sProfileImagePath = PortedHelpers.GetLocalPath(PortedHelpers.LCropStr(sSource, "?"), sLocalPathRoot); + } + } + } + catch + { + } + } + + if (!sCssClass.StartsWith("container", StringComparison.Ordinal)) + { + continue; + } + + var dTrauerfall = new Dictionary(StringComparer.Ordinal) + { + ["profImg"] = sProfileImagePath, + ["filter"] = xAnnouncementNode["filter"]?.ToString() ?? string.Empty + }; + + var sId = xAnnouncementNode["id"]?.ToString() ?? string.Empty; + var arrSplit = sId.Split('_', 2); + dTrauerfall["id"] = arrSplit.Length > 1 ? arrSplit[1] : string.Empty; + + if (xAnnouncementNode["text"] is JsonArray arrAnnouncementText && arrAnnouncementText.Count > 1) + { + var sPublish = arrAnnouncementText[1]?.ToString() ?? string.Empty; + if (sPublish.StartsWith("vom", StringComparison.Ordinal)) + { + dTrauerfall["publish"] = sPublish.Length > 4 ? sPublish[4..] : string.Empty; + } + } + + if (xTrauerfallData["name"] is not null) + { + dTrauerfall["name"] = xTrauerfallData["name"]!.ToString(); + } + + if (arrTrauerfallSections.Count > 0 && arrTrauerfallSections[0] is JsonObject xFirstSection && xFirstSection["text"] is JsonArray arrFirstText && arrFirstText.Count > 1) + { + var sBirthName = arrFirstText[1]?.ToString() ?? string.Empty; + if (sBirthName.StartsWith("geb.", StringComparison.Ordinal)) + { + dTrauerfall["Birthname"] = sBirthName.Length > 5 ? sBirthName[5..] : string.Empty; + } + } + + foreach (var sKey in new[] { "url", "Birth", "Death", "Place", "created_by", "created_on", "visits" }) + { + if (xTrauerfallData[sKey] is not null) + { + dTrauerfall[sKey] = xTrauerfallData[sKey]!.ToString(); + } + } + + if (xAnnouncementNode["links"] is JsonArray arrAnnouncementLinks && arrAnnouncementLinks.Count >= 3) + { + var sImage = arrAnnouncementLinks[1]?["href"]?.ToString() ?? string.Empty; + dTrauerfall["img"] = PortedHelpers.GetLocalPath(sImage, sLocalPathRoot); + var sPdf = arrAnnouncementLinks[2]?["href"]?.ToString() ?? string.Empty; + dTrauerfall["pdf"] = PortedHelpers.GetLocalPath(sPdf, sLocalPathRoot); + if (xTrauerfallData[sPdf] is JsonObject xPdfObject && xPdfObject["pdfText"] is not null) + { + dTrauerfall["pdfText"] = xPdfObject["pdfText"]!.ToString(); + } + else + { + var sPdfFile = dTrauerfall["pdf"]?.ToString() ?? string.Empty; + if (File.Exists(sPdfFile)) + { + dTrauerfall["pdfText"] = PortedHelpers.PdfText(File.ReadAllBytes(sPdfFile)); + } + } + } + + arrResult.Add(dTrauerfall); + } + } + + return arrResult; + } + + /// + /// Reads obituary rows by internal id. + /// + public List> TrauerAnzId(int iId) + { + return Query("SELECT * FROM Anzeigen WHERE idAnzeige=@id", xCommand => xCommand.Parameters.AddWithValue("@id", iId)); + } + + /// + /// Reads obituary rows by announcement id. + /// + public List> TrauerAnz(int iAnnouncement) + { + return Query("SELECT * FROM Anzeigen WHERE Announcement=@announcement", xCommand => xCommand.Parameters.AddWithValue("@announcement", iAnnouncement)); + } + + /// + /// Reads legacy obituary rows by legacy order id. + /// + public List> LegacyTrauerAnz(string sAuftrag) + { + return Query("SELECT * FROM `RNZ-Traueranzeigen`.`Anzeigen` WHERE Auftrag=@auftrag", xCommand => xCommand.Parameters.AddWithValue("@auftrag", sAuftrag)); + } + + /// + /// Reads obituary rows where the specified field is null. + /// + public List> TrauerAnzIsNull(string sField, int iLimit = 1) + { + return Query($"SELECT * FROM `Anzeigen` WHERE `{sField}` is null limit @limit", xCommand => xCommand.Parameters.AddWithValue("@limit", iLimit)); + } + + /// + /// Reads obituary case rows where the specified field is null. + /// + public List> TrauerFallIsNull(string sField, int iLimit = 1) + { + return Query($"SELECT * FROM `Trauerfall` WHERE `{sField}` is null limit @limit", xCommand => xCommand.Parameters.AddWithValue("@limit", iLimit)); + } + + /// + /// Reads obituary case rows where the specified field matches the provided value. + /// + public List> TrauerFallEquals(string sField, string sValue, int iLimit = 1) + { + return Query($"SELECT * FROM `Trauerfall` WHERE `{sField}`=@value limit @limit", xCommand => + { + xCommand.Parameters.AddWithValue("@value", sValue); + xCommand.Parameters.AddWithValue("@limit", iLimit); + }); + } + + /// + /// Updates obituary case rows when values have changed. + /// + public void UpdateTrauerFall(List> arrNewValues, List> arrOldValues) + { + UpdateRows("Trauerfall", arrNewValues, arrOldValues); + } + + /// + /// Updates obituary announcement rows when values have changed. + /// + public bool UpdateTrauerAnz(List> arrNewValues, List> arrOldValues) + { + return UpdateRows("Anzeigen", arrNewValues, arrOldValues); + } + + /// + /// Reads obituary case rows by internal id. + /// + public List> TrauerFallById(int iId) + { + return Query("SELECT * FROM Trauerfall WHERE idTrauerfall=@id", xCommand => xCommand.Parameters.AddWithValue("@id", iId)); + } + + /// + /// Reads obituary case rows by URL. + /// + public List> TrauerFallByUrl(string sUrl) + { + return Query("SELECT * FROM Trauerfall WHERE url=@url", xCommand => xCommand.Parameters.AddWithValue("@url", sUrl)); + } + + /// + /// Builds an in-memory index of obituary case URLs. + /// + public void BuildTrauerFallIndex() + { + TfIdx = new Dictionary(StringComparer.Ordinal); + using var xCommand = new MySqlCommand("SELECT idTrauerfall,url FROM Trauerfall", _dbConn); + using var xReader = xCommand.ExecuteReader(); + while (xReader.Read()) + { + TfIdx[xReader.GetString(1)] = xReader.GetInt64(0); + } + } + + /// + /// Inserts a new obituary case row. + /// + public long AppendTrauerFall(Dictionary dTrauerfall) + { + var (sLastName, sFirstName) = PortedHelpers.SplitName(PortedHelpers.Cond(dTrauerfall, "name")); + using var xCommand = new MySqlCommand( + "INSERT INTO `Trauerfall` (`URL`, `Created`, `Preread_Birth`, `Preread_Death`, `Fullname`, `Firstname`, `Lastname`, `Birthname`, `Place`, `Created_by`) VALUES (@url, @created, @birth, @death, @fullName, @firstName, @lastName, @birthName, @place, @createdBy);", + _dbConn); + xCommand.Parameters.AddWithValue("@url", PortedHelpers.Cond(dTrauerfall, "url")); + xCommand.Parameters.AddWithValue("@created", ToDbValue(PortedHelpers.Str2Date(PortedHelpers.Cond(dTrauerfall, "created_on")))); + xCommand.Parameters.AddWithValue("@birth", ToDbValue(PortedHelpers.Str2Date(TrimLeadingTwo(PortedHelpers.Cond(dTrauerfall, "Birth"))))); + xCommand.Parameters.AddWithValue("@death", ToDbValue(PortedHelpers.Str2Date(TrimLeadingTwo(PortedHelpers.Cond(dTrauerfall, "Death"))))); + xCommand.Parameters.AddWithValue("@fullName", $"{sLastName}, {sFirstName}"); + xCommand.Parameters.AddWithValue("@firstName", sFirstName); + xCommand.Parameters.AddWithValue("@lastName", sLastName); + xCommand.Parameters.AddWithValue("@birthName", PortedHelpers.Cond(dTrauerfall, "Birthname")); + xCommand.Parameters.AddWithValue("@place", PortedHelpers.Cond(dTrauerfall, "Place")); + xCommand.Parameters.AddWithValue("@createdBy", PortedHelpers.Cond(dTrauerfall, "created_by")); + xCommand.ExecuteNonQuery(); + return xCommand.LastInsertedId; + } + + /// + /// Inserts a new obituary announcement row. + /// + public long AppendTrauerAnz(long iTrauerfallId, Dictionary dTrauerfall, string sLocalPath) + { + var sPath = Directory.GetParent(PortedHelpers.Cond(dTrauerfall, "img"))?.FullName ?? string.Empty; + var sNormalizedLocalPath = sPath.Replace(sLocalPath, string.Empty, StringComparison.Ordinal); + var sProfileBase = Directory.GetParent(Directory.GetParent(sPath)?.FullName ?? string.Empty)?.FullName ?? string.Empty; + var sProfileImage = PortedHelpers.Cond(dTrauerfall, "profImg").Replace(sProfileBase, "..\\..", StringComparison.Ordinal); + var iRubrik = GetRubrik(dTrauerfall); + var (sLastName, sFirstName) = PortedHelpers.SplitName(PortedHelpers.Cond(dTrauerfall, "name")); + + using var xCommand = new MySqlCommand( + "INSERT INTO `Anzeigen` (`idTrauerfall`, `url`, `Announcement`, `release`,`localpath`, `pngFile`, `pdfFile`, `Additional`, `Firstname`,`Lastname`, `Birthname`, `Birth`, `Death`, `Place`, `Info`, `ProfileImg`, `Rubrik`) VALUES (@idtf, @url, @announcement, @release, @localpath, @pngFile, @pdfFile, @additional, @firstName, @lastName, @birthName, @birth, @death, @place, @info, @profileImg, @rubrik);", + _dbConn); + xCommand.Parameters.AddWithValue("@idtf", iTrauerfallId); + xCommand.Parameters.AddWithValue("@url", PortedHelpers.Cond(dTrauerfall, "url")); + xCommand.Parameters.AddWithValue("@announcement", int.TryParse(PortedHelpers.Cond(dTrauerfall, "id"), out var iId) ? iId : 0); + xCommand.Parameters.AddWithValue("@release", ToDbValue(PortedHelpers.Str2Date(PortedHelpers.Cond(dTrauerfall, "publish")))); + xCommand.Parameters.AddWithValue("@localpath", sNormalizedLocalPath); + xCommand.Parameters.AddWithValue("@pngFile", Path.GetFileName(PortedHelpers.Cond(dTrauerfall, "img"))); + xCommand.Parameters.AddWithValue("@pdfFile", Path.GetFileName(PortedHelpers.Cond(dTrauerfall, "pdf"))); + xCommand.Parameters.AddWithValue("@additional", JsonSerializer.Serialize(dTrauerfall, PortedHelpers.JsonOptions)); + xCommand.Parameters.AddWithValue("@firstName", sFirstName); + xCommand.Parameters.AddWithValue("@lastName", sLastName); + xCommand.Parameters.AddWithValue("@birthName", PortedHelpers.Cond(dTrauerfall, "Birthname")); + xCommand.Parameters.AddWithValue("@birth", ToDbValue(PortedHelpers.Str2Date(TrimLeadingTwo(PortedHelpers.Cond(dTrauerfall, "Birth"))))); + xCommand.Parameters.AddWithValue("@death", ToDbValue(PortedHelpers.Str2Date(TrimLeadingTwo(PortedHelpers.Cond(dTrauerfall, "Death"))))); + xCommand.Parameters.AddWithValue("@place", PortedHelpers.Cond(dTrauerfall, "Place")); + xCommand.Parameters.AddWithValue("@info", PortedHelpers.Cond(dTrauerfall, "pdfText")); + xCommand.Parameters.AddWithValue("@profileImg", sProfileImage); + xCommand.Parameters.AddWithValue("@rubrik", iRubrik); + xCommand.ExecuteNonQuery(); + return xCommand.LastInsertedId; + } + + /// + /// Applies announcement values to the current row dictionary. + /// + public void SetTrauerAnz(Dictionary dCurrent, Dictionary dTrauerfall, string sLocalPath) + { + var sPath = Directory.GetParent(PortedHelpers.Cond(dTrauerfall, "img"))?.FullName ?? string.Empty; + var sNormalizedLocalPath = sPath.Replace(sLocalPath, string.Empty, StringComparison.Ordinal); + var sProfileBase = Directory.GetParent(Directory.GetParent(sPath)?.FullName ?? string.Empty)?.FullName ?? string.Empty; + var sProfileImage = PortedHelpers.Cond(dTrauerfall, "profImg").Replace(sProfileBase, "..\\..", StringComparison.Ordinal); + var iRubrik = dCurrent.TryGetValue("Rubrik", out var xCurrentRubrik) && int.TryParse(Convert.ToString(xCurrentRubrik, CultureInfo.InvariantCulture), out var iParsed) ? iParsed : 8050; + var sFilter = PortedHelpers.Cond(dTrauerfall, "filter"); + if (sFilter == "danksagungen") + { + iRubrik = 8060; + } + else if (sFilter == "nachrufe") + { + iRubrik = 8070; + } + else if (sFilter == "todesanzeigen") + { + iRubrik = 8050; + } + else + { + try + { + var xJson = JsonNode.Parse(Convert.ToString(dCurrent.GetValueOrDefault("Additional"), CultureInfo.InvariantCulture) ?? string.Empty) as JsonObject; + if (xJson?["filter"] is not null) + { + dTrauerfall["filter"] = xJson["filter"]!.ToString(); + } + } + catch + { + } + } + + var (sLastName, sFirstName) = PortedHelpers.SplitName(PortedHelpers.Cond(dTrauerfall, "name")); + foreach (var kvPair in new Dictionary + { + ["url"] = PortedHelpers.Cond(dTrauerfall, "url"), + ["Announcement"] = int.TryParse(PortedHelpers.Cond(dTrauerfall, "id"), out var iId) ? iId : 0, + ["release"] = ToDbValue(PortedHelpers.Str2Date(PortedHelpers.Cond(dTrauerfall, "publish"))), + ["localpath"] = sNormalizedLocalPath, + ["pngFile"] = Path.GetFileName(PortedHelpers.Cond(dTrauerfall, "img")), + ["pdfFile"] = Path.GetFileName(PortedHelpers.Cond(dTrauerfall, "pdf")), + ["Additional"] = JsonSerializer.Serialize(dTrauerfall, PortedHelpers.JsonOptions), + ["Firstname"] = sFirstName, + ["Lastname"] = sLastName, + ["Birthname"] = PortedHelpers.Cond(dTrauerfall, "Birthname"), + ["Birth"] = ToDbValue(PortedHelpers.Str2Date(TrimLeadingTwo(PortedHelpers.Cond(dTrauerfall, "Birth")))), + ["Death"] = ToDbValue(PortedHelpers.Str2Date(TrimLeadingTwo(PortedHelpers.Cond(dTrauerfall, "Death")))), + ["Place"] = PortedHelpers.Cond(dTrauerfall, "Place"), + ["Info"] = PortedHelpers.Cond(dTrauerfall, "pdfText"), + ["ProfileImg"] = sProfileImage, + ["Rubrik"] = iRubrik + }) + { + dCurrent[kvPair.Key] = kvPair.Value; + } + } + + /// + /// Inserts a legacy obituary announcement row. + /// + public long AppendLegacyTAnz(string sAuftrag, Dictionary dTrauerfall, string sLocalPath) + { + var sNormalizedLocalPath = Directory.GetParent(PortedHelpers.Cond(dTrauerfall, "pdf"))?.FullName?.Replace(sLocalPath, string.Empty, StringComparison.Ordinal) ?? string.Empty; + using var xCommand = new MySqlCommand( + "INSERT INTO `RNZ-Traueranzeigen`.`Anzeigen` (`Auftrag`, `url`, `Announcement`, `release`,`localpath`, `pngFile`, `pdfFile`, `Additional`) VALUES (@auftrag, @url, @announcement, @release, @localpath, @pngFile, @pdfFile, @additional);", + _dbConn); + xCommand.Parameters.AddWithValue("@auftrag", sAuftrag); + xCommand.Parameters.AddWithValue("@url", PortedHelpers.Cond(dTrauerfall, "url")); + xCommand.Parameters.AddWithValue("@announcement", int.TryParse(PortedHelpers.Cond(dTrauerfall, "id"), out var iId) ? iId : 0); + xCommand.Parameters.AddWithValue("@release", ToDbValue(PortedHelpers.Str2Date(PortedHelpers.Cond(dTrauerfall, "publish")))); + xCommand.Parameters.AddWithValue("@localpath", sNormalizedLocalPath); + xCommand.Parameters.AddWithValue("@pngFile", Path.GetFileName(PortedHelpers.Cond(dTrauerfall, "img"))); + xCommand.Parameters.AddWithValue("@pdfFile", Path.GetFileName(PortedHelpers.Cond(dTrauerfall, "pdf"))); + xCommand.Parameters.AddWithValue("@additional", JsonSerializer.Serialize(dTrauerfall, PortedHelpers.JsonOptions)); + xCommand.ExecuteNonQuery(); + return xCommand.LastInsertedId; + } + + /// + /// Imports extracted obituary data into the database. + /// + public void TrauerDataToDb(IEnumerable> arrData, string sLocalPath) + { + foreach (var dAnnouncement in arrData) + { + var arrCurrentCases = TrauerFallByUrl(PortedHelpers.Cond(dAnnouncement, "url")); + var iTrauerfallId = arrCurrentCases.Count == 0 + ? AppendTrauerFall(dAnnouncement) + : Convert.ToInt64(arrCurrentCases[0].Values.First(), CultureInfo.InvariantCulture); + + var arrCurrentAnnouncements = TrauerAnz(int.TryParse(PortedHelpers.Cond(dAnnouncement, "id"), out var iId) ? iId : 0); + if (arrCurrentAnnouncements.Count == 0) + { + Console.Write('+'); + AppendTrauerAnz(iTrauerfallId, dAnnouncement, sLocalPath); + } + else + { + Console.Write('-'); + var arrCopy = arrCurrentAnnouncements.Select(d => new Dictionary(d, StringComparer.Ordinal)).ToList(); + SetTrauerAnz(arrCopy[0], dAnnouncement, sLocalPath); + if (UpdateTrauerAnz(arrCopy, arrCurrentAnnouncements)) + { + Console.Write("\bx"); + } + } + } + } + + /// + public void Dispose() + { + _dbConn.Dispose(); + } + + private bool UpdateRows(string sTable, List> arrNewValues, List> arrOldValues) + { + var xChanged = false; + if (arrNewValues.Count == 0) + { + return false; + } + + var sKeyField = arrNewValues[0].Keys.FirstOrDefault(k => k.StartsWith("id", StringComparison.OrdinalIgnoreCase)); + if (string.IsNullOrEmpty(sKeyField)) + { + return false; + } + + var dOldById = arrOldValues.ToDictionary(x => Convert.ToString(x[sKeyField], CultureInfo.InvariantCulture) ?? string.Empty, StringComparer.Ordinal); + foreach (var dNewRow in arrNewValues) + { + var sKey = Convert.ToString(dNewRow[sKeyField], CultureInfo.InvariantCulture) ?? string.Empty; + if (!dOldById.TryGetValue(sKey, out var dOldRow)) + { + continue; + } + + foreach (var (sColumn, xValue) in dNewRow) + { + dOldRow.TryGetValue(sColumn, out var xOldValue); + if (Equals(xOldValue, xValue)) + { + continue; + } + + using var xCommand = new MySqlCommand($"UPDATE `{sTable}` SET `{sColumn}`=@value WHERE `{sKeyField}`=@key", _dbConn); + xCommand.Parameters.AddWithValue("@value", xValue ?? DBNull.Value); + xCommand.Parameters.AddWithValue("@key", dNewRow[sKeyField]); + xCommand.ExecuteNonQuery(); + xChanged = true; + } + } + + return xChanged; + } + + private List> Query(string sSql, Action xBind) + { + var arrData = new List>(); + using var xCommand = new MySqlCommand(sSql, _dbConn); + xBind(xCommand); + using var xReader = xCommand.ExecuteReader(); + while (xReader.Read()) + { + var dRow = new Dictionary(StringComparer.Ordinal); + for (var iIndex = 0; iIndex < xReader.FieldCount; iIndex++) + { + dRow[xReader.GetName(iIndex)] = xReader.IsDBNull(iIndex) + ? null + : xReader.GetValue(iIndex) switch + { + DateTime dtValue => dtValue.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture), + _ => xReader.GetValue(iIndex) + }; + } + + arrData.Add(dRow); + } + + return arrData; + } + + private static object ToDbValue(DateOnly? dtValue) + { + return dtValue.HasValue + ? dtValue.Value.ToDateTime(TimeOnly.MinValue) + : DBNull.Value; + } + + private static string TrimLeadingTwo(string sValue) + { + return sValue.Length > 2 ? sValue[2..] : string.Empty; + } + + private static int GetRubrik(IReadOnlyDictionary dTrauerfall) + { + return PortedHelpers.Cond(dTrauerfall, "filter") switch + { + "danksagungen" => 8060, + "nachrufe" => 8070, + _ => 8050 + }; + } +} diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/PortedHelpers.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/PortedHelpers.cs new file mode 100644 index 000000000..8506800d7 --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/PortedHelpers.cs @@ -0,0 +1,288 @@ +using System.Globalization; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using UglyToad.PdfPig; + +namespace RnzTrauer.Core; + +/// +/// Contains helper methods ported from the original Python implementation. +/// +public static class PortedHelpers +{ + /// + /// Gets the shared JSON serializer options used by the ported tools. + /// + public static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = true, + PropertyNameCaseInsensitive = true + }; + + /// + /// Converts a RNZ date string in dd.MM.yyyy format into a value. + /// + public static DateOnly? Str2Date(string? sValue) + { + if (string.IsNullOrWhiteSpace(sValue)) + { + return null; + } + + var arrParts = sValue.Split('.'); + if (arrParts.Length != 3) + { + return null; + } + + if (int.TryParse(arrParts[2], out var iYear) && + int.TryParse(arrParts[1], out var iMonth) && + int.TryParse(arrParts[0], out var iDay)) + { + try + { + return new DateOnly(iYear, iMonth, iDay); + } + catch + { + return null; + } + } + + return null; + } + + /// + /// Returns a trimmed string value from a loosely typed dictionary. + /// + public static string Cond(IReadOnlyDictionary dValues, string sKey) + { + return dValues.TryGetValue(sKey, out var xValue) + ? Convert.ToString(xValue, CultureInfo.InvariantCulture)?.Trim(' ') ?? string.Empty + : string.Empty; + } + + /// + /// Crops the input string at the first occurrence of the specified separator. + /// + public static string LCropStr(string sOriginal, string sSeparator) + { + var iFound = sOriginal.IndexOf(sSeparator, StringComparison.Ordinal); + return iFound >= 0 ? sOriginal[..iFound] : sOriginal; + } + + /// + /// Splits a full name into last name and first name using the original Python rules. + /// + public static (string LastName, string FirstName) SplitName(string sName) + { + var arrNames = sName.Trim(' ').Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray(); + if (arrNames.Length == 0) + { + return (string.Empty, string.Empty); + } + + for (var iIndex = arrNames.Length - 1; iIndex >= 0; iIndex--) + { + if (arrNames[iIndex].Equals("von", StringComparison.OrdinalIgnoreCase) || arrNames[iIndex].Equals("van", StringComparison.OrdinalIgnoreCase)) + { + arrNames[iIndex] = arrNames[iIndex].ToLowerInvariant(); + if (iIndex < arrNames.Length - 1) + { + arrNames[iIndex + 1] = $"{arrNames[iIndex]} {arrNames[iIndex + 1]}"; + arrNames[iIndex] = string.Empty; + } + } + else if (arrNames[iIndex].Contains('-', StringComparison.Ordinal)) + { + var arrParts = arrNames[iIndex].Split('-', 2); + arrNames[iIndex] = $"{Capitalize(arrParts[0])}-{Capitalize(arrParts.Length > 1 ? arrParts[1] : string.Empty)}"; + } + else + { + arrNames[iIndex] = Capitalize(arrNames[iIndex]); + } + } + + var sLastName = arrNames[^1]; + var sFirstName = string.Join(" ", arrNames[..^1]).Trim(' '); + return (sLastName, sFirstName); + } + + /// + /// Rewrites absolute page references into relative local paths. + /// + public static string MakeLocal(string sSource, string sReference) + { + var iSlashCount = sReference.Count(c => c == '/'); + const string sPrefix = "../../../../../"; + var sRelative = sPrefix[..Math.Max(0, Math.Min(sPrefix.Length, (iSlashCount - 3) * 3))]; + sSource = sSource.Replace("src=\"/", $"src=\"{sRelative}", StringComparison.Ordinal); + sSource = sSource.Replace("href=\"/", $"href=\"{sRelative}", StringComparison.Ordinal); + var sRest = sReference.Length > 9 ? sReference[9..] : string.Empty; + var iIndex = sRest.IndexOf('/', StringComparison.Ordinal); + var sRoot = iIndex >= 0 && sReference.Length >= iIndex + 10 ? sReference[..(iIndex + 10)] : sReference; + sSource = sSource.Replace(sRoot, sRelative, StringComparison.Ordinal); + sSource = sSource.Replace(".jpg", ".png", StringComparison.OrdinalIgnoreCase); + return sSource; + } + + /// + /// Maps a remote RNZ URL to the local file system path used by the Python scripts. + /// + public static string GetLocalPath(string sUrl, string sLocalPathRoot, DateOnly? dtReference = null) + { + var dtCurrent = dtReference ?? DateOnly.FromDateTime(DateTime.Today); + var sLocalPath = sUrl.Replace("https://trauer.rnz.de", string.Empty, StringComparison.Ordinal) + .Replace("/", "\\", StringComparison.Ordinal); + + var iQueryIndex = sLocalPath.IndexOf('?', StringComparison.Ordinal); + if (iQueryIndex >= 0) + { + sLocalPath = sLocalPath[..iQueryIndex]; + } + + if (sLocalPath.Contains("aktuelle-ausgabe", StringComparison.Ordinal)) + { + sLocalPath = sLocalPath.Replace( + "suche\\aktuelle-ausgabe", + $"suche\\erscheinungstag-{dtCurrent.Day}-{dtCurrent.Month}-{dtCurrent.Year}", + StringComparison.Ordinal); + } + + var iFound = sLocalPath.IndexOf("erscheinungstag", StringComparison.Ordinal); + if (iFound >= 0) + { + sLocalPath = sLocalPath.Replace("\\seite", "-pg", StringComparison.Ordinal); + foreach (var sAnnouncementType in new[] { "nachrufe", "danksagungen", "todesanzeigen", "_" }) + { + sLocalPath = sLocalPath.Replace($"\\anzeigenart-{sAnnouncementType}", $"-{sAnnouncementType[..1]}", StringComparison.Ordinal); + } + + if (sLocalPath.Contains("tag-heute", StringComparison.Ordinal)) + { + sLocalPath = sLocalPath.Replace( + "traueranzeigen-suche\\erscheinungstag", + $"{dtCurrent.Year}\\{dtCurrent:yyyy-MM-dd}\\liste", + StringComparison.Ordinal); + } + else + { + var iStartIndex = Math.Min(sLocalPath.Length, iFound + 16); + var sDateFragment = LCropStr(sLocalPath.Substring(iStartIndex, Math.Min(10, Math.Max(0, sLocalPath.Length - iStartIndex))), "\\"); + var arrSplit = sDateFragment.Split('-'); + if (arrSplit.Length >= 3) + { + sLocalPath = sLocalPath.Replace( + "traueranzeigen-suche\\erscheinungstag", + $"{arrSplit[2]}\\{arrSplit[2]}-{arrSplit[1].PadLeft(2, '0')}-{arrSplit[0].PadLeft(2, '0')}\\liste", + StringComparison.Ordinal); + } + } + } + + const string sMarker = "traueranzeige\\"; + iFound = sLocalPath.IndexOf(sMarker, StringComparison.Ordinal); + if (iFound >= 0) + { + var sPathPart = LCropStr(sLocalPath[(iFound + sMarker.Length)..], "\\"); + using var xMd5 = MD5.Create(); + var arrDigest = xMd5.ComputeHash(Encoding.UTF8.GetBytes(sPathPart)); + var iCrc = (((int)arrDigest[^2] << 8) | arrDigest[^1]) & 1023; + sLocalPath = sLocalPath.Replace(sMarker, $"{sMarker}{iCrc:X3}\\", StringComparison.Ordinal); + } + + return sLocalPathRoot + sLocalPath; + } + + /// + /// Extracts text from a PDF byte array. + /// + public static string PdfText(byte[] arrBytes) + { + try + { + using var xStream = new MemoryStream(arrBytes); + using var xDocument = PdfDocument.Open(xStream); + var sBuilder = new StringBuilder(); + foreach (var xPage in xDocument.GetPages()) + { + sBuilder.AppendLine(xPage.Text); + } + + return sBuilder.ToString(); + } + catch + { + return string.Empty; + } + } + + /// + /// Converts a supported CLR value into a . + /// + public static JsonNode? ToJsonNode(object? xValue) + { + return xValue switch + { + null => null, + JsonNode xNode => xNode.DeepClone(), + string sValue => JsonValue.Create(sValue), + bool xValue1 => JsonValue.Create(xValue1), + int iValue => JsonValue.Create(iValue), + long iValue => JsonValue.Create(iValue), + double fValue => JsonValue.Create(fValue), + decimal fValue => JsonValue.Create(fValue), + DateOnly dtValue => JsonValue.Create(dtValue.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)), + DateTime dtValue => JsonValue.Create(dtValue.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)), + byte[] arrBytes => JsonValue.Create(Convert.ToBase64String(arrBytes)), + IDictionary dValues => ToJsonObject(new Dictionary(dValues, StringComparer.Ordinal)), + IDictionary dValues => ToJsonObject(dValues.ToDictionary(k => k.Key, v => (object?)v.Value)), + IEnumerable> arrItems => ToJsonArray(arrItems.Cast()), + IEnumerable arrItems when xValue is not string => ToJsonArray(arrItems), + _ => JsonSerializer.SerializeToNode(xValue, JsonOptions) + }; + } + + /// + /// Converts a dictionary into a JSON object. + /// + public static JsonObject ToJsonObject(IReadOnlyDictionary dValues) + { + var xObject = new JsonObject(); + foreach (var kvValue in dValues) + { + xObject[kvValue.Key] = ToJsonNode(kvValue.Value); + } + + return xObject; + } + + private static JsonArray ToJsonArray(IEnumerable arrItems) + { + var xArray = new JsonArray(); + foreach (var xItem in arrItems) + { + xArray.Add(ToJsonNode(xItem)); + } + + return xArray; + } + + private static string Capitalize(string sValue) + { + if (string.IsNullOrEmpty(sValue)) + { + return sValue; + } + + if (sValue.Length == 1) + { + return sValue.ToUpperInvariant(); + } + + return char.ToUpperInvariant(sValue[0]) + sValue[1..].ToLowerInvariant(); + } +} diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/WebHandler.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/WebHandler.cs new file mode 100644 index 000000000..3216541c9 --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/WebHandler.cs @@ -0,0 +1,530 @@ +using System.Globalization; +using System.Text; +using OpenQA.Selenium; +using OpenQA.Selenium.Firefox; + +namespace RnzTrauer.Core; + +/// +/// Provides Selenium-based scraping for the RNZ obituary portal. +/// +public sealed class WebHandler : IDisposable +{ + /// + /// Gets the data payload key used by the ported dictionaries. + /// + public const string CsData = "Data"; + + /// + /// Gets the header payload key used by the ported dictionaries. + /// + public const string CsHeader = "Header"; + + /// + /// Gets the media source key used by the ported dictionaries. + /// + public const string CsSrc = "Src"; + + private static readonly Dictionary _tagAttributes = new(StringComparer.OrdinalIgnoreCase) + { + ["section"] = ["class", "id"], + ["div"] = ["class", "id"], + ["img"] = ["class", "title", "alt", "src", "style", "data-original"], + ["a"] = ["title", "target", "href"] + }; + + private readonly HttpClient _httpClient = new(); + private readonly RnzConfig _config; + + /// + /// Initializes a new instance of the class. + /// + public WebHandler(RnzConfig xConfig) + { + _config = xConfig; + } + + /// + /// Gets the active Firefox driver instance. + /// + public FirefoxDriver? Driver { get; private set; } + + /// + /// Initializes the RNZ login session. + /// + public void InitPage() + { + try + { + Driver?.Quit(); + } + catch + { + } + + var xOptions = new FirefoxOptions(); + Driver = new FirefoxDriver(xOptions); + Driver.Navigate().GoToUrl(_config.Url); + Driver.FindElement(By.Id("emailAddress")).SendKeys(_config.User); + Driver.FindElement(By.Id("password")).SendKeys(_config.Password); + Driver.FindElement(By.Id("form")).Submit(); + while (Driver.Title == _config.Title || string.IsNullOrEmpty(Driver.Title)) + { + Thread.Sleep(500); + } + + Thread.Sleep(500); + } + + /// + /// Recursively captures matching elements into dictionary-based structures. + /// + public List> Wdr2List(ISearchContext xWebElement, WebQuery xQuery) + { + var arrResult = new List>(); + foreach (var xElement in xWebElement.FindElements(xQuery.By)) + { + var dItem = new Dictionary + { + ["tag"] = xElement.TagName + }; + arrResult.Add(dItem); + + if (_tagAttributes.TryGetValue(xElement.TagName, out var arrAttributes)) + { + foreach (var sAttribute in arrAttributes) + { + try + { + dItem[sAttribute] = xElement.GetAttribute(sAttribute) ?? string.Empty; + } + catch + { + dItem[sAttribute] = string.Empty; + } + } + } + + try + { + dItem["text"] = (xElement.Text ?? string.Empty).Split(Environment.NewLine, StringSplitOptions.None).ToList(); + } + catch + { + } + + foreach (var xChildQuery in xQuery.Children) + { + dItem[xChildQuery.Name] = Wdr2List(xElement, xChildQuery); + } + } + + return arrResult; + } + + /// + /// Loads the configured RNZ pages and associated media. + /// + public (Dictionary> Pages, List> Items) GetData1(string sStart, int iMaxPage = 30) + { + var xDriver = Driver ?? throw new InvalidOperationException("The web driver has not been initialized."); + var dPages = new Dictionary>(StringComparer.Ordinal); + var arrItems = new List>(); + + foreach (var sAnnouncementType in new[] { "nachrufe", "danksagungen", "todesanzeigen", "_" }) + { + xDriver.Navigate().GoToUrl($"{sStart}/anzeigenart-{sAnnouncementType}"); + while (xDriver.Title == "RNZ" || string.IsNullOrEmpty(xDriver.Title)) + { + Thread.Sleep(500); + } + + var sNextUrl = "."; + var iCounter = 0; + while (!string.IsNullOrEmpty(sNextUrl) && iCounter < iMaxPage) + { + Console.WriteLine(xDriver.Url); + var (arrSubPages, sUrl, sNext) = WorkMainPage(dPages, arrItems, sAnnouncementType); + sNextUrl = sNext; + + Console.Write("\nGet Subpages:"); + foreach (var sSubPage in arrSubPages) + { + xDriver.Navigate().GoToUrl(sSubPage + "/anzeigen"); + Thread.Sleep(5500); + WorkSubPage(sUrl, dPages, arrItems); + } + + Console.WriteLine(); + if (!string.IsNullOrEmpty(sNextUrl)) + { + xDriver.Navigate().GoToUrl(sNextUrl); + while (xDriver.Url == sUrl) + { + Thread.Sleep(500); + Console.Write('.'); + } + } + + iCounter++; + } + } + + return (dPages, arrItems); + } + + /// + /// Closes the browser session. + /// + public void Close() + { + Driver?.Quit(); + Driver = null; + } + + /// + public void Dispose() + { + Close(); + _httpClient.Dispose(); + } + + private (List SubPages, string Url, string NextUrl) WorkMainPage(Dictionary> dPages, List> arrItems, string sAnnouncementType) + { + var xDriver = Driver ?? throw new InvalidOperationException("The web driver has not been initialized."); + var sUrl = xDriver.Url; + var dPage = new Dictionary + { + ["Title"] = xDriver.Title, + ["url"] = sUrl, + ["filter"] = sAnnouncementType, + ["sections"] = new List>(), + ["content"] = PortedHelpers.MakeLocal(xDriver.PageSource, sUrl.Length > 50 ? sUrl[..50] + "/" : sUrl + "/") + }; + var arrSubPages = new List(); + dPages[sUrl] = dPage; + + var arrElements = Wdr2List(xDriver, new WebQuery(string.Empty, By.ClassName("c-blockitem"), new WebQuery("links", By.TagName("a")), new WebQuery("imgs", By.TagName("img")))); + dPage["sections"] = arrElements; + var sNextUrl = string.Empty; + + foreach (var dElement in arrElements) + { + var arrText = GetStringList(dElement, "text"); + if (arrText.Count > 1 && arrText[0].StartsWith("ANZ", StringComparison.Ordinal)) + { + var dAnnouncement = new Dictionary + { + ["Title"] = arrText[0].Length >= 8 ? arrText[0][8..] : arrText[0], + ["parent"] = sUrl, + ["Text"] = arrText.Cast().ToList() + }; + + if (arrText.Count > 1) + { + dAnnouncement["Info"] = arrText[1]; + } + + var arrLinks = GetDictionaryList(dElement, "links"); + if (arrLinks.Count > 0) + { + var sHref = Convert.ToString(arrLinks[0].GetValueOrDefault("href"), CultureInfo.InvariantCulture) ?? string.Empty; + dAnnouncement["href"] = sHref; + arrSubPages.Add(sHref); + } + + var arrImages = GetDictionaryList(dElement, "imgs"); + if (arrImages.Count > 0) + { + foreach (var dImage in arrImages) + { + var dCopy = new Dictionary(dAnnouncement, StringComparer.Ordinal); + dCopy[CsSrc] = string.Empty; + var sSource = Convert.ToString(dImage.GetValueOrDefault("src"), CultureInfo.InvariantCulture) ?? string.Empty; + var sDataOriginal = Convert.ToString(dImage.GetValueOrDefault("data-original"), CultureInfo.InvariantCulture) ?? string.Empty; + if (sSource.Contains("MEDIASERVER", StringComparison.Ordinal)) + { + dCopy[CsSrc] = sSource; + } + else if (sDataOriginal.Contains("MEDIASERVER", StringComparison.Ordinal)) + { + dCopy[CsSrc] = sDataOriginal; + } + + Console.Write('.'); + try + { + var sMediaSource = Convert.ToString(dCopy[CsSrc], CultureInfo.InvariantCulture) ?? string.Empty; + if (!string.IsNullOrEmpty(sMediaSource)) + { + var xResponse = _httpClient.GetAsync(sMediaSource).GetAwaiter().GetResult(); + dCopy[CsData] = xResponse.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult(); + dCopy[CsHeader] = xResponse.Headers.Concat(xResponse.Content.Headers) + .ToDictionary(h => h.Key, h => string.Join(",", h.Value), StringComparer.OrdinalIgnoreCase); + if (sMediaSource.Contains("MEDIASERVER", StringComparison.Ordinal)) + { + dPage[sMediaSource] = new Dictionary + { + [CsHeader] = (Dictionary)dCopy[CsHeader]! + }; + } + } + } + catch + { + dCopy[CsData] = Array.Empty(); + } + + arrItems.Add(dCopy); + } + } + else + { + arrItems.Add(dAnnouncement); + } + } + else if (arrText.Count > 1 && string.Join(" ", arrText).Contains('>') && string.IsNullOrEmpty(sNextUrl)) + { + var arrLinks = GetDictionaryList(dElement, "links"); + foreach (var dLink in arrLinks) + { + var arrLinkText = GetStringList(dLink, "text"); + if (arrLinkText.Count == 1 && arrLinkText[0] == ">") + { + sNextUrl = Convert.ToString(dLink.GetValueOrDefault("href"), CultureInfo.InvariantCulture) ?? string.Empty; + } + } + + Console.WriteLine(sNextUrl); + } + } + + return (arrSubPages, sUrl, sNextUrl); + } + + private void WorkSubPage(string sUrl, Dictionary> dPages, List> arrItems) + { + var xDriver = Driver ?? throw new InvalidOperationException("The web driver has not been initialized."); + var dPage = dPages[sUrl]; + var sSubPageUrl = xDriver.Url; + if (!dPages.TryGetValue(sSubPageUrl, out var dPage2)) + { + dPage2 = new Dictionary + { + ["parent"] = new List() + }; + dPages[sSubPageUrl] = dPage2; + } + + Dictionary dParentSection = new(StringComparer.Ordinal); + var arrPageSections = GetDictionaryList(dPage, "sections"); + for (var iIndex = 0; iIndex < arrPageSections.Count; iIndex++) + { + var dSection = arrPageSections[iIndex]; + var arrLinks = GetDictionaryList(dSection, "links"); + var sHref = arrLinks.Count > 0 ? Convert.ToString(arrLinks[0].GetValueOrDefault("href"), CultureInfo.InvariantCulture) ?? string.Empty : string.Empty; + if (!string.IsNullOrEmpty(sHref) && sHref + "/anzeigen" == sSubPageUrl) + { + dParentSection = dSection; + arrPageSections[iIndex] = dSection; + } + } + + dPage2["Title"] = xDriver.Title; + dPage2["url"] = sSubPageUrl; + if (dPage2["parent"] is not List arrParents) + { + arrParents = new List(); + dPage2["parent"] = arrParents; + } + + arrParents.Add(sUrl); + dPage2["content"] = PortedHelpers.MakeLocal(xDriver.PageSource, sSubPageUrl + '/'); + var arrMedia = new List(); + dPage2["media"] = arrMedia; + var arrSections = Wdr2List(xDriver, new WebQuery(string.Empty, By.TagName("section"), new WebQuery("links", By.TagName("a")), new WebQuery("imgs", By.TagName("img")))); + dPage2["sections"] = arrSections; + + foreach (var xElement in xDriver.FindElements(By.TagName("h1"))) + { + dPage2["name"] = xElement.Text; + } + + foreach (var xElement in xDriver.FindElements(By.ClassName("col-sm-6"))) + { + if (xElement.Text.StartsWith("*", StringComparison.Ordinal)) + { + dPage2["Birth"] = xElement.Text; + } + else if (xElement.Text.Contains('†')) + { + var arrParts = xElement.Text.Split("in", 2, StringSplitOptions.None); + dPage2["Death"] = arrParts[0]; + dPage2["Place"] = arrParts.Length > 1 ? arrParts[1] : string.Empty; + } + } + + Console.Write('.'); + foreach (var dSection in arrSections) + { + var sSectionClass = Convert.ToString(dSection.GetValueOrDefault("class"), CultureInfo.InvariantCulture) ?? string.Empty; + if (sSectionClass.StartsWith("col-12", StringComparison.Ordinal)) + { + foreach (var sLine in GetStringList(dSection, "text")) + { + if (sLine.StartsWith("Erstellt", StringComparison.Ordinal)) + { + dPage2["created_by"] = sLine.Length > 12 ? sLine[12..] : string.Empty; + } + else if (sLine.StartsWith("Angelegt", StringComparison.Ordinal)) + { + dPage2["created_on"] = sLine.Length > 11 ? sLine[11..] : string.Empty; + } + else if (sLine.EndsWith("Besuche", StringComparison.Ordinal)) + { + dPage2["visits"] = sLine.Length > 7 ? sLine[..^7] : string.Empty; + } + } + + foreach (var dImage in GetDictionaryList(dSection, "imgs")) + { + var sHref = Convert.ToString(dImage.GetValueOrDefault("src"), CultureInfo.InvariantCulture) ?? string.Empty; + var sText = Convert.ToString(dImage.GetValueOrDefault("alt"), CultureInfo.InvariantCulture) ?? string.Empty; + if (sHref.Contains("MEDIASERVER", StringComparison.Ordinal) && !arrMedia.Contains(sHref)) + { + arrMedia.Add(sHref); + var dAnnouncement = new Dictionary + { + ["parent"] = sSubPageUrl, + [CsSrc] = sHref, + ["Info"] = sText, + ["id"] = string.Empty + }; + + TryLoadBinary(sHref, dAnnouncement); + arrItems.Add(dAnnouncement); + } + } + } + else if (!sSectionClass.StartsWith("col-xl-8", StringComparison.Ordinal)) + { + foreach (var dImage in GetDictionaryList(dSection, "imgs")) + { + var sSource = Convert.ToString(dImage.GetValueOrDefault("src"), CultureInfo.InvariantCulture) ?? string.Empty; + var sDataOriginal = Convert.ToString(dImage.GetValueOrDefault("data-original"), CultureInfo.InvariantCulture) ?? string.Empty; + if (!string.IsNullOrEmpty(sSource) && dPage.ContainsKey(sSource)) + { + dParentSection["id-anz"] = Convert.ToString(dSection.GetValueOrDefault("id"), CultureInfo.InvariantCulture) ?? string.Empty; + if (dPage[sSource] is Dictionary dPageMedia) + { + dPageMedia["id-anz"] = dParentSection["id-anz"]; + } + + dSection["filter"] = dPage["filter"]; + } + else if (!string.IsNullOrEmpty(sDataOriginal) && dPage.ContainsKey($"https://trauer.rnz.de{sDataOriginal}")) + { + dParentSection["id-anz"] = Convert.ToString(dSection.GetValueOrDefault("id"), CultureInfo.InvariantCulture) ?? string.Empty; + if (dPage[$"https://trauer.rnz.de{sDataOriginal}"] is Dictionary dPageMedia) + { + dPageMedia["id-anz"] = dParentSection["id-anz"]; + } + + dSection["filter"] = dPage["filter"]; + } + } + + foreach (var dLink in GetDictionaryList(dSection, "links")) + { + try + { + var sHref = Convert.ToString(dLink.GetValueOrDefault("href"), CultureInfo.InvariantCulture) ?? string.Empty; + var arrText = GetStringList(dLink, "text"); + if (sHref.Contains("MEDIASERVER", StringComparison.Ordinal) && !arrMedia.Contains(sHref)) + { + arrMedia.Add(sHref); + } + + if ((arrText.Count == 1 && arrText[0] == "Speichern") || (arrText.Count == 1 && arrText[0] == "Großansicht")) + { + var dAnnouncement = new Dictionary + { + ["parent"] = sSubPageUrl, + [CsSrc] = sHref, + ["id"] = Convert.ToString(dSection.GetValueOrDefault("id"), CultureInfo.InvariantCulture) ?? string.Empty + }; + + if (Equals(dParentSection.GetValueOrDefault("id-anz"), dAnnouncement["id"])) + { + var arrImages = GetDictionaryList(dParentSection, "imgs"); + arrImages.Add(new Dictionary(dAnnouncement)); + dParentSection["imgs"] = arrImages; + } + + TryLoadBinary(sHref, dAnnouncement); + if (sHref.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) && dAnnouncement.TryGetValue(CsData, out var xDataObject) && xDataObject is byte[] arrBinary) + { + var sPrefix = Encoding.ASCII.GetString(arrBinary.Take(10).ToArray()); + if (sPrefix.Contains("PNG", StringComparison.Ordinal)) + { + dLink["href"] = sHref.Replace(".jpg", ".png", StringComparison.OrdinalIgnoreCase); + } + } + + arrItems.Add(dAnnouncement); + } + } + catch + { + } + } + } + } + } + + private void TryLoadBinary(string sHref, Dictionary dTarget) + { + try + { + var xResponse = _httpClient.GetAsync(sHref).GetAwaiter().GetResult(); + dTarget[CsData] = xResponse.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult(); + dTarget[CsHeader] = xResponse.Headers.Concat(xResponse.Content.Headers) + .ToDictionary(h => h.Key, h => string.Join(",", h.Value), StringComparer.OrdinalIgnoreCase); + } + catch + { + dTarget[CsData] = Array.Empty(); + dTarget[CsHeader] = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + } + + private static List> GetDictionaryList(IReadOnlyDictionary dValues, string sKey) + { + if (!dValues.TryGetValue(sKey, out var xValue) || xValue is null) + { + return []; + } + + return xValue switch + { + List> arrReady => arrReady, + IEnumerable> arrEnumerable => arrEnumerable.ToList(), + _ => [] + }; + } + + private static List GetStringList(IReadOnlyDictionary dValues, string sKey) + { + if (!dValues.TryGetValue(sKey, out var xValue) || xValue is null) + { + return []; + } + + return xValue switch + { + List arrList => arrList.Select(v => Convert.ToString(v, CultureInfo.InvariantCulture) ?? string.Empty).ToList(), + IEnumerable arrStringList => arrStringList.ToList(), + _ => [] + }; + } +} diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/PortedHelpersTests.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/PortedHelpersTests.cs new file mode 100644 index 000000000..ceae44593 --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/PortedHelpersTests.cs @@ -0,0 +1,62 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using RnzTrauer.Core; + +namespace RnzTrauer.Tests; + +[TestClass] +public sealed class PortedHelpersTests +{ + private const string LocalRoot = "\\\\Diskstation\\Daten\\dokumente\\HTML\\trauer.rnz.de"; + + [TestMethod] + public void LCropStr_Returns_Left_Part() + { + Assert.AreEqual("123", PortedHelpers.LCropStr("1234567", "4")); + } + + [DataTestMethod] + [DataRow("A B", "B", "A")] + [DataRow("aa bb", "Bb", "Aa")] + [DataRow(" a b c ", "C", "A B")] + [DataRow(" aü b-c ", "B-C", "Aü")] + [DataRow("üa bö-cä", "Bö-Cä", "Üa")] + [DataRow("üa öb-äc", "Öb-Äc", "Üa")] + [DataRow(" a von c ", "von C", "A")] + [DataRow(" aa bb-cc ", "Bb-Cc", "Aa")] + public void SplitName_Matches_Python_Behaviour(string input, string expectedLastName, string expectedFirstName) + { + var (lastName, firstName) = PortedHelpers.SplitName(input); + Assert.AreEqual(expectedLastName, lastName); + Assert.AreEqual(expectedFirstName, firstName); + } + + [DataTestMethod] + [DataRow("https://trauer.rnz.de/MEDIASERVER/content/LH230/obi_new/2022_11/waltraud-hacker-traueranzeige-697fddce-93c0-4b07-af61-bb046a47cbba.jpg", "\\\\Diskstation\\Daten\\dokumente\\HTML\\trauer.rnz.de\\MEDIASERVER\\content\\LH230\\obi_new\\2022_11\\waltraud-hacker-traueranzeige-697fddce-93c0-4b07-af61-bb046a47cbba.jpg")] + [DataRow("https://trauer.rnz.de/traueranzeigen-suche/erscheinungstag-01-10-2021", "\\\\Diskstation\\Daten\\dokumente\\HTML\\trauer.rnz.de\\2021\\2021-10-01\\liste-01-10-2021")] + [DataRow("https://trauer.rnz.de/traueranzeigen-suche/erscheinungstag-01-10-2021/seite-3", "\\\\Diskstation\\Daten\\dokumente\\HTML\\trauer.rnz.de\\2021\\2021-10-01\\liste-01-10-2021-pg-3")] + [DataRow("https://trauer.rnz.de/traueranzeigen-suche/erscheinungstag-3-8-2021/anzeigenart-danksagungen/seite-3", "\\\\Diskstation\\Daten\\dokumente\\HTML\\trauer.rnz.de\\2021\\2021-08-03\\liste-3-8-2021-d-pg-3")] + [DataRow("https://trauer.rnz.de/traueranzeige/aktuelle-ausgabe?code=934598745894569734506987456304567", "\\\\Diskstation\\Daten\\dokumente\\HTML\\trauer.rnz.de\\traueranzeige\\22D\\aktuelle-ausgabe")] + public void GetLocalPath_Matches_Known_Paths(string url, string expected) + { + var actual = PortedHelpers.GetLocalPath(url, LocalRoot, new DateOnly(2024, 12, 24)); + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void GetLocalPath_Handles_AktuelleAusgabe_And_Heute() + { + var day = new DateOnly(2024, 12, 24); + Assert.AreEqual($"{LocalRoot}\\2024\\2024-12-24\\liste-24-12-2024", PortedHelpers.GetLocalPath("https://trauer.rnz.de/traueranzeigen-suche/aktuelle-ausgabe", LocalRoot, day)); + Assert.AreEqual($"{LocalRoot}\\2024\\2024-12-24\\liste-heute-pg-2", PortedHelpers.GetLocalPath("https://trauer.rnz.de/traueranzeigen-suche/erscheinungstag-heute/seite-2", LocalRoot, day)); + } + + [DataTestMethod] + [DataRow("", "https://www.jc99.de/test/text2", "")] + [DataRow("", "https://www.jc99.de/test/test/text2", "")] + [DataRow("", "https://www.jc99.de/test/text2", "")] + [DataRow("", "https://www.jc99.de/test/test/text2", "")] + public void MakeLocal_Matches_Python_Behaviour(string source, string reference, string expected) + { + Assert.AreEqual(expected, PortedHelpers.MakeLocal(source, reference)); + } +} diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/RnzTrauer.Tests.csproj b/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/RnzTrauer.Tests.csproj new file mode 100644 index 000000000..80a5e8447 --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/RnzTrauer.Tests.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + enable + enable + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + \ No newline at end of file From 01a6e8ff2d25a70f6bb285dbe7612efe90228d4b Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 00:36:12 +0200 Subject: [PATCH 09/96] GenDBImplOLEDBTests --- GenFreeWin/GenDBImplOLEDBTests/GenDBImplOLEDBTests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GenFreeWin/GenDBImplOLEDBTests/GenDBImplOLEDBTests.csproj b/GenFreeWin/GenDBImplOLEDBTests/GenDBImplOLEDBTests.csproj index 6e234a376..4c46c4cca 100644 --- a/GenFreeWin/GenDBImplOLEDBTests/GenDBImplOLEDBTests.csproj +++ b/GenFreeWin/GenDBImplOLEDBTests/GenDBImplOLEDBTests.csproj @@ -14,8 +14,8 @@ - - + + From b42dd38a9bbb0db0aec73266080824bedd16eb9f Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 00:36:13 +0200 Subject: [PATCH 10/96] GenFreeBaseClassesTests --- .../GenFreeBaseClassesTests/GenFreeBaseClassesTests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GenFreeWin/GenFreeBaseClassesTests/GenFreeBaseClassesTests.csproj b/GenFreeWin/GenFreeBaseClassesTests/GenFreeBaseClassesTests.csproj index 9f2cb504c..efed7a5b8 100644 --- a/GenFreeWin/GenFreeBaseClassesTests/GenFreeBaseClassesTests.csproj +++ b/GenFreeWin/GenFreeBaseClassesTests/GenFreeBaseClassesTests.csproj @@ -8,8 +8,8 @@ - - + + From 76dac52036bba7e60c9e6685d352473fdb60ef4e Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 00:36:13 +0200 Subject: [PATCH 11/96] GenFreeBaseTests --- GenFreeWin/GenFreeBaseTests/GenFreeBaseTests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GenFreeWin/GenFreeBaseTests/GenFreeBaseTests.csproj b/GenFreeWin/GenFreeBaseTests/GenFreeBaseTests.csproj index bbf473860..c06f5d81c 100644 --- a/GenFreeWin/GenFreeBaseTests/GenFreeBaseTests.csproj +++ b/GenFreeWin/GenFreeBaseTests/GenFreeBaseTests.csproj @@ -15,8 +15,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive From c3378d93b6e3fbe653cf252c4ee32c42d2d1d020 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 00:36:13 +0200 Subject: [PATCH 12/96] GenFreeBrowser.Tests --- GenFreeWin/GenFreeBrowser.Tests/GenFreeBrowser.Tests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GenFreeWin/GenFreeBrowser.Tests/GenFreeBrowser.Tests.csproj b/GenFreeWin/GenFreeBrowser.Tests/GenFreeBrowser.Tests.csproj index 900d43f97..d0c730c8e 100644 --- a/GenFreeWin/GenFreeBrowser.Tests/GenFreeBrowser.Tests.csproj +++ b/GenFreeWin/GenFreeBrowser.Tests/GenFreeBrowser.Tests.csproj @@ -12,8 +12,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all From 04eb0dc2ae6f615e386dace54214f35173658687 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 00:36:14 +0200 Subject: [PATCH 13/96] GenFreeDataTests --- GenFreeWin/GenFreeDataTests/GenFreeDataTests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GenFreeWin/GenFreeDataTests/GenFreeDataTests.csproj b/GenFreeWin/GenFreeDataTests/GenFreeDataTests.csproj index e00aac8a2..758bc3c8f 100644 --- a/GenFreeWin/GenFreeDataTests/GenFreeDataTests.csproj +++ b/GenFreeWin/GenFreeDataTests/GenFreeDataTests.csproj @@ -18,8 +18,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive From fb8fe77d53a3e91352607858cd9e583dd8408c68 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 00:36:14 +0200 Subject: [PATCH 14/96] GenFreeHelperTests --- GenFreeWin/GenFreeHelperTests/GenFreeHelperTests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GenFreeWin/GenFreeHelperTests/GenFreeHelperTests.csproj b/GenFreeWin/GenFreeHelperTests/GenFreeHelperTests.csproj index 198807ab0..03e6f312c 100644 --- a/GenFreeWin/GenFreeHelperTests/GenFreeHelperTests.csproj +++ b/GenFreeWin/GenFreeHelperTests/GenFreeHelperTests.csproj @@ -19,8 +19,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive From adb39f8a1af4823792732fa487442c5f0967b927 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 00:36:16 +0200 Subject: [PATCH 15/96] GenFreeWinFormsTests --- GenFreeWin/GenFreeWinFormsTests/GenFreeWinFormsTests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GenFreeWin/GenFreeWinFormsTests/GenFreeWinFormsTests.csproj b/GenFreeWin/GenFreeWinFormsTests/GenFreeWinFormsTests.csproj index 9191926af..e0e9e797e 100644 --- a/GenFreeWin/GenFreeWinFormsTests/GenFreeWinFormsTests.csproj +++ b/GenFreeWin/GenFreeWinFormsTests/GenFreeWinFormsTests.csproj @@ -11,8 +11,8 @@ - - + + From 4839a593d7d346e87b844012b4a3488f4b2e58e9 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 00:36:16 +0200 Subject: [PATCH 16/96] GenFreeWinTests --- GenFreeWin/GenFreeWinTests/genFreeWinTests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GenFreeWin/GenFreeWinTests/genFreeWinTests.csproj b/GenFreeWin/GenFreeWinTests/genFreeWinTests.csproj index 487496f70..02f332b42 100644 --- a/GenFreeWin/GenFreeWinTests/genFreeWinTests.csproj +++ b/GenFreeWin/GenFreeWinTests/genFreeWinTests.csproj @@ -19,8 +19,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive From a20f8898e3356269befac69f1b4e408b79d13dba Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 00:36:17 +0200 Subject: [PATCH 17/96] GenSecure.Core.Tests --- GenFreeWin/GenSecure.Core.Tests/GenSecure.Core.Tests.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/GenFreeWin/GenSecure.Core.Tests/GenSecure.Core.Tests.csproj b/GenFreeWin/GenSecure.Core.Tests/GenSecure.Core.Tests.csproj index 2b2f77066..0c49fd2ab 100644 --- a/GenFreeWin/GenSecure.Core.Tests/GenSecure.Core.Tests.csproj +++ b/GenFreeWin/GenSecure.Core.Tests/GenSecure.Core.Tests.csproj @@ -1,4 +1,4 @@ - + net9.0-windows @@ -14,8 +14,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all From 4c80312deb724234a263cd016d8a35b1465d7e19 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 00:36:18 +0200 Subject: [PATCH 18/96] MdbBrowserTests --- GenFreeWin/MdbBrowserTests/MdbBrowserTests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GenFreeWin/MdbBrowserTests/MdbBrowserTests.csproj b/GenFreeWin/MdbBrowserTests/MdbBrowserTests.csproj index 8b2e8dca0..15b827150 100644 --- a/GenFreeWin/MdbBrowserTests/MdbBrowserTests.csproj +++ b/GenFreeWin/MdbBrowserTests/MdbBrowserTests.csproj @@ -18,8 +18,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive From 5fcea7f29b66058f30a8c9c05f4f76634b1b8be8 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 00:36:20 +0200 Subject: [PATCH 19/96] VBUnObfusicatorTests --- GenFreeWin/VBUnObfusicatorTests/VBUnObfusicatorTests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GenFreeWin/VBUnObfusicatorTests/VBUnObfusicatorTests.csproj b/GenFreeWin/VBUnObfusicatorTests/VBUnObfusicatorTests.csproj index 99034f930..3461297cd 100644 --- a/GenFreeWin/VBUnObfusicatorTests/VBUnObfusicatorTests.csproj +++ b/GenFreeWin/VBUnObfusicatorTests/VBUnObfusicatorTests.csproj @@ -23,8 +23,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive From b04de51ee698f5ae953b4dde4dff1aa28a744232 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 00:36:21 +0200 Subject: [PATCH 20/96] BaseGenClassesTests --- WinAhnenNew/BaseGenClassesTests/BaseGenClassesTests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WinAhnenNew/BaseGenClassesTests/BaseGenClassesTests.csproj b/WinAhnenNew/BaseGenClassesTests/BaseGenClassesTests.csproj index 93dafacf2..f0e5f7b1c 100644 --- a/WinAhnenNew/BaseGenClassesTests/BaseGenClassesTests.csproj +++ b/WinAhnenNew/BaseGenClassesTests/BaseGenClassesTests.csproj @@ -15,7 +15,7 @@ $(TargetFrameworks);net10.0 - + all From 18dc4c76c75ccd42d546c23eded4b12acd6ae802 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 00:36:22 +0200 Subject: [PATCH 21/96] WinAhnenClsTests --- WinAhnenNew/WinAhnenClsTests/WinAhnenClsTests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WinAhnenNew/WinAhnenClsTests/WinAhnenClsTests.csproj b/WinAhnenNew/WinAhnenClsTests/WinAhnenClsTests.csproj index a126a6571..63ac706ce 100644 --- a/WinAhnenNew/WinAhnenClsTests/WinAhnenClsTests.csproj +++ b/WinAhnenNew/WinAhnenClsTests/WinAhnenClsTests.csproj @@ -17,8 +17,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive From 13d5c3130582631223d0dd17b356e891827c4b93 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 00:36:23 +0200 Subject: [PATCH 22/96] WinAhnenNew.Model.Tests --- .../WinAhnenNew.Model.Tests/WinAhnenNew.Model.Tests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WinAhnenNew/WinAhnenNew.Model.Tests/WinAhnenNew.Model.Tests.csproj b/WinAhnenNew/WinAhnenNew.Model.Tests/WinAhnenNew.Model.Tests.csproj index 67d73afcc..ff6cea9c1 100644 --- a/WinAhnenNew/WinAhnenNew.Model.Tests/WinAhnenNew.Model.Tests.csproj +++ b/WinAhnenNew/WinAhnenNew.Model.Tests/WinAhnenNew.Model.Tests.csproj @@ -10,8 +10,8 @@ - - + + From 16627dfeebdf4dd042337ce797d9fcb6846373b7 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 00:36:23 +0200 Subject: [PATCH 23/96] WinAhnenNew.UI.Tests --- WinAhnenNew/WinAhnenNew.UI.Tests/WinAhnenNew.UI.Tests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WinAhnenNew/WinAhnenNew.UI.Tests/WinAhnenNew.UI.Tests.csproj b/WinAhnenNew/WinAhnenNew.UI.Tests/WinAhnenNew.UI.Tests.csproj index 6fdd62cfa..75b205c95 100644 --- a/WinAhnenNew/WinAhnenNew.UI.Tests/WinAhnenNew.UI.Tests.csproj +++ b/WinAhnenNew/WinAhnenNew.UI.Tests/WinAhnenNew.UI.Tests.csproj @@ -10,8 +10,8 @@ - - + + From 88e65518695bbbb14805cbc3257ea573ed8b7127 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 00:36:23 +0200 Subject: [PATCH 24/96] GenFreeWin --- GenFreeWin/Gen_FreeWin.sln | 66 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/GenFreeWin/Gen_FreeWin.sln b/GenFreeWin/Gen_FreeWin.sln index a7ab67025..9ae226669 100644 --- a/GenFreeWin/Gen_FreeWin.sln +++ b/GenFreeWin/Gen_FreeWin.sln @@ -296,6 +296,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinAhnenNew.Model.Tests", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinAhnenNew.UI.Tests", "..\WinAhnenNew\WinAhnenNew.UI.Tests\WinAhnenNew.UI.Tests.csproj", "{CFBAA8EF-C46B-9F97-97FF-9D08F68ECE58}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RnzTrauer", "RnzTrauer", "{9257CE06-4B6A-44D1-869C-445CBFCDFF1D}" + ProjectSection(SolutionItems) = preProject + ..\WinAhnenNew\RnzTrauer\Directory.Build.props = ..\WinAhnenNew\RnzTrauer\Directory.Build.props + ..\WinAhnenNew\RnzTrauer\Directory.Packages.props = ..\WinAhnenNew\RnzTrauer\Directory.Packages.props + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RnzTrauer.Core", "..\WinAhnenNew\RnzTrauer\RnzTrauer.Core\RnzTrauer.Core.csproj", "{C550598C-93A6-237F-E1A5-37106948D110}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RnzTrauer.Tests", "..\WinAhnenNew\RnzTrauer\RnzTrauer.Tests\RnzTrauer.Tests.csproj", "{A176D9D1-803C-21F9-9CC0-0A4E9E3067A0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RnzTrauer.Console", "..\WinAhnenNew\RnzTrauer\RnzTrauer.Console\RnzTrauer.Console.csproj", "{E46C404F-B6FE-7940-6A5F-C8CE98B13CFC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AmtsblattLoader.Console", "..\WinAhnenNew\RnzTrauer\AmtsblattLoader.Console\AmtsblattLoader.Console.csproj", "{E0654505-59DC-B459-B483-7EA9602FB041}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1192,6 +1206,54 @@ Global {CFBAA8EF-C46B-9F97-97FF-9D08F68ECE58}.Release|x64.Build.0 = Release|Any CPU {CFBAA8EF-C46B-9F97-97FF-9D08F68ECE58}.Release|x86.ActiveCfg = Release|Any CPU {CFBAA8EF-C46B-9F97-97FF-9D08F68ECE58}.Release|x86.Build.0 = Release|Any CPU + {C550598C-93A6-237F-E1A5-37106948D110}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C550598C-93A6-237F-E1A5-37106948D110}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C550598C-93A6-237F-E1A5-37106948D110}.Debug|x64.ActiveCfg = Debug|Any CPU + {C550598C-93A6-237F-E1A5-37106948D110}.Debug|x64.Build.0 = Debug|Any CPU + {C550598C-93A6-237F-E1A5-37106948D110}.Debug|x86.ActiveCfg = Debug|Any CPU + {C550598C-93A6-237F-E1A5-37106948D110}.Debug|x86.Build.0 = Debug|Any CPU + {C550598C-93A6-237F-E1A5-37106948D110}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C550598C-93A6-237F-E1A5-37106948D110}.Release|Any CPU.Build.0 = Release|Any CPU + {C550598C-93A6-237F-E1A5-37106948D110}.Release|x64.ActiveCfg = Release|Any CPU + {C550598C-93A6-237F-E1A5-37106948D110}.Release|x64.Build.0 = Release|Any CPU + {C550598C-93A6-237F-E1A5-37106948D110}.Release|x86.ActiveCfg = Release|Any CPU + {C550598C-93A6-237F-E1A5-37106948D110}.Release|x86.Build.0 = Release|Any CPU + {A176D9D1-803C-21F9-9CC0-0A4E9E3067A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A176D9D1-803C-21F9-9CC0-0A4E9E3067A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A176D9D1-803C-21F9-9CC0-0A4E9E3067A0}.Debug|x64.ActiveCfg = Debug|Any CPU + {A176D9D1-803C-21F9-9CC0-0A4E9E3067A0}.Debug|x64.Build.0 = Debug|Any CPU + {A176D9D1-803C-21F9-9CC0-0A4E9E3067A0}.Debug|x86.ActiveCfg = Debug|Any CPU + {A176D9D1-803C-21F9-9CC0-0A4E9E3067A0}.Debug|x86.Build.0 = Debug|Any CPU + {A176D9D1-803C-21F9-9CC0-0A4E9E3067A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A176D9D1-803C-21F9-9CC0-0A4E9E3067A0}.Release|Any CPU.Build.0 = Release|Any CPU + {A176D9D1-803C-21F9-9CC0-0A4E9E3067A0}.Release|x64.ActiveCfg = Release|Any CPU + {A176D9D1-803C-21F9-9CC0-0A4E9E3067A0}.Release|x64.Build.0 = Release|Any CPU + {A176D9D1-803C-21F9-9CC0-0A4E9E3067A0}.Release|x86.ActiveCfg = Release|Any CPU + {A176D9D1-803C-21F9-9CC0-0A4E9E3067A0}.Release|x86.Build.0 = Release|Any CPU + {E46C404F-B6FE-7940-6A5F-C8CE98B13CFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E46C404F-B6FE-7940-6A5F-C8CE98B13CFC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E46C404F-B6FE-7940-6A5F-C8CE98B13CFC}.Debug|x64.ActiveCfg = Debug|Any CPU + {E46C404F-B6FE-7940-6A5F-C8CE98B13CFC}.Debug|x64.Build.0 = Debug|Any CPU + {E46C404F-B6FE-7940-6A5F-C8CE98B13CFC}.Debug|x86.ActiveCfg = Debug|Any CPU + {E46C404F-B6FE-7940-6A5F-C8CE98B13CFC}.Debug|x86.Build.0 = Debug|Any CPU + {E46C404F-B6FE-7940-6A5F-C8CE98B13CFC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E46C404F-B6FE-7940-6A5F-C8CE98B13CFC}.Release|Any CPU.Build.0 = Release|Any CPU + {E46C404F-B6FE-7940-6A5F-C8CE98B13CFC}.Release|x64.ActiveCfg = Release|Any CPU + {E46C404F-B6FE-7940-6A5F-C8CE98B13CFC}.Release|x64.Build.0 = Release|Any CPU + {E46C404F-B6FE-7940-6A5F-C8CE98B13CFC}.Release|x86.ActiveCfg = Release|Any CPU + {E46C404F-B6FE-7940-6A5F-C8CE98B13CFC}.Release|x86.Build.0 = Release|Any CPU + {E0654505-59DC-B459-B483-7EA9602FB041}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E0654505-59DC-B459-B483-7EA9602FB041}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E0654505-59DC-B459-B483-7EA9602FB041}.Debug|x64.ActiveCfg = Debug|Any CPU + {E0654505-59DC-B459-B483-7EA9602FB041}.Debug|x64.Build.0 = Debug|Any CPU + {E0654505-59DC-B459-B483-7EA9602FB041}.Debug|x86.ActiveCfg = Debug|Any CPU + {E0654505-59DC-B459-B483-7EA9602FB041}.Debug|x86.Build.0 = Debug|Any CPU + {E0654505-59DC-B459-B483-7EA9602FB041}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E0654505-59DC-B459-B483-7EA9602FB041}.Release|Any CPU.Build.0 = Release|Any CPU + {E0654505-59DC-B459-B483-7EA9602FB041}.Release|x64.ActiveCfg = Release|Any CPU + {E0654505-59DC-B459-B483-7EA9602FB041}.Release|x64.Build.0 = Release|Any CPU + {E0654505-59DC-B459-B483-7EA9602FB041}.Release|x86.ActiveCfg = Release|Any CPU + {E0654505-59DC-B459-B483-7EA9602FB041}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1274,6 +1336,10 @@ Global {4CBA4707-37E9-F26E-1A6D-7FE5DEC40EE7} = {2A2E129C-95B6-4192-BA94-D9C5A249AB43} {020CBE92-89AC-FBA2-7546-79AAF62859F0} = {2A2E129C-95B6-4192-BA94-D9C5A249AB43} {CFBAA8EF-C46B-9F97-97FF-9D08F68ECE58} = {2A2E129C-95B6-4192-BA94-D9C5A249AB43} + {C550598C-93A6-237F-E1A5-37106948D110} = {9257CE06-4B6A-44D1-869C-445CBFCDFF1D} + {A176D9D1-803C-21F9-9CC0-0A4E9E3067A0} = {9257CE06-4B6A-44D1-869C-445CBFCDFF1D} + {E46C404F-B6FE-7940-6A5F-C8CE98B13CFC} = {9257CE06-4B6A-44D1-869C-445CBFCDFF1D} + {E0654505-59DC-B459-B483-7EA9602FB041} = {9257CE06-4B6A-44D1-869C-445CBFCDFF1D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3B766B89-B32C-4777-9290-8BF7CEC401B5} From f3207d4f802d5ec892b48fbe5da4c4a1aa27bbc1 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 02:39:37 +0200 Subject: [PATCH 25/96] DAO --- .../GenFreeBase/Interfaces/Model/IPrintDat.cs | 69 +++ .../RnzTrauer.Core/Services/FileProxy.cs | 37 ++ .../Services/FirefoxWebDriverFactory.cs | 17 + .../Services/HttpClientProxy.cs | 21 + .../RnzTrauer.Core/Services/IConfigLoader.cs | 12 + .../RnzTrauer.Core/Services/IFile.cs | 32 ++ .../Services/IHttpClientProxy.cs | 12 + .../Services/IWebDriverFactory.cs | 14 + .../Services/WebHandlerProgress.cs | 6 + .../RnzTrauer.Tests/ConfigLoaderTests.cs | 136 ++++++ .../PortedHelpersAdditionalTests.cs | 129 +++++ .../RnzTrauer.Tests/WebHandlerTests.cs | 458 ++++++++++++++++++ 12 files changed, 943 insertions(+) create mode 100644 GenFreeWin/GenFreeBase/Interfaces/Model/IPrintDat.cs create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/FileProxy.cs create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/FirefoxWebDriverFactory.cs create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/HttpClientProxy.cs create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/IConfigLoader.cs create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/IFile.cs create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/IHttpClientProxy.cs create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/IWebDriverFactory.cs create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/WebHandlerProgress.cs create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Tests/ConfigLoaderTests.cs create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Tests/PortedHelpersAdditionalTests.cs create mode 100644 WinAhnenNew/RnzTrauer/RnzTrauer.Tests/WebHandlerTests.cs diff --git a/GenFreeWin/GenFreeBase/Interfaces/Model/IPrintDat.cs b/GenFreeWin/GenFreeBase/Interfaces/Model/IPrintDat.cs new file mode 100644 index 000000000..fb015528c --- /dev/null +++ b/GenFreeWin/GenFreeBase/Interfaces/Model/IPrintDat.cs @@ -0,0 +1,69 @@ +using GenFree.Data; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GenFree.Interfaces.Model; + +/// +/// Interface for print-specific data and methods used only in Druck project. +/// Accessed via IModul1.PrintDat property. +/// +public interface IPrintDat +{ + /// Gets or sets the superscript offset for citations. + int Hoch { get; set; } + + /// Gets or sets the count of remarks/citations. + int BemZahl { get; set; } + + /// Gets the remarks/citations container. + IList KontBem { get; } + + /// Gets the temporary storage for Kont values. + IList KontSP { get; } + + /// Gets the temporary storage for Kont1 values. + IList KontSP1 { get; } + + /// Gets or sets the family save variable. + int FamSp { get; set; } + + /// Gets or sets the godparent flag. + bool Pat { get; set; } + + /// Gets or sets the flag switch for printing. + int Flagsch { get; set; } + + /// Gets or sets the ancestor number length. + byte KonLen { get; set; } + + /// Gets or sets general yes/no flag. + int Ja { get; set; } + + /// Gets or sets the sequence number. + byte LfNR { get; set; } + + /// Removes trailing newlines and spaces. + string Retweg(string sText); + + /// Adds person to name index. + void Namenindex(long lAhne); + + /// Reads person source/citation data. + void PerQu(ref int iFamPer); + + /// Reads source data for date. + void QuellenDatum(ref int iNr, EEventArt eArt, ref short iLfNR); + + /// Reads source data with dot notation. + void QuelledotnDatum(ref int iNr, EEventArt eArt, ref short iLfNR); + + /// Reads family residence data. + void FWohn(EEventArt eArt, ref short iListart); + + /// Reads witness data for person. + void Zeuge_Bei(int iPersInArb, ref short iListart); +} diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/FileProxy.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/FileProxy.cs new file mode 100644 index 000000000..4a2e119a4 --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/FileProxy.cs @@ -0,0 +1,37 @@ +namespace RnzTrauer.Core; + +/// +/// Delegates file-system operations to . +/// +public sealed class FileProxy : IFile +{ + /// + public bool Exists(string sPath) + { + return File.Exists(sPath); + } + + /// + public string ReadAllText(string sPath) + { + return File.ReadAllText(sPath); + } + + /// + public byte[] ReadAllBytes(string sPath) + { + return File.ReadAllBytes(sPath); + } + + /// + public void WriteAllText(string sPath, string sContents) + { + File.WriteAllText(sPath, sContents); + } + + /// + public void WriteAllBytes(string sPath, byte[] arrBytes) + { + File.WriteAllBytes(sPath, arrBytes); + } +} diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/FirefoxWebDriverFactory.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/FirefoxWebDriverFactory.cs new file mode 100644 index 000000000..7a2aaa093 --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/FirefoxWebDriverFactory.cs @@ -0,0 +1,17 @@ +using OpenQA.Selenium; +using OpenQA.Selenium.Firefox; + +namespace RnzTrauer.Core; + +/// +/// Creates Firefox-based Selenium web drivers. +/// +public sealed class FirefoxWebDriverFactory : IWebDriverFactory +{ + /// + public IWebDriver Create() + { + var xOptions = new FirefoxOptions(); + return new FirefoxDriver(xOptions); + } +} diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/HttpClientProxy.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/HttpClientProxy.cs new file mode 100644 index 000000000..d157b7f8f --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/HttpClientProxy.cs @@ -0,0 +1,21 @@ +namespace RnzTrauer.Core; + +/// +/// Delegates HTTP operations to . +/// +public sealed class HttpClientProxy : IHttpClientProxy +{ + private readonly HttpClient _xHttpClient = new(); + + /// + public Task GetAsync(string sRequestUri) + { + return _xHttpClient.GetAsync(sRequestUri); + } + + /// + public void Dispose() + { + _xHttpClient.Dispose(); + } +} diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/IConfigLoader.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/IConfigLoader.cs new file mode 100644 index 000000000..1dc632865 --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/IConfigLoader.cs @@ -0,0 +1,12 @@ +namespace RnzTrauer.Core; + +/// +/// Provides JSON-based configuration loading for the ported tools. +/// +public interface IConfigLoader +{ + /// + /// Loads a configuration instance from the specified JSON file. + /// + T Load(string sFilePath) where T : new(); +} diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/IFile.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/IFile.cs new file mode 100644 index 000000000..ba44a6fd1 --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/IFile.cs @@ -0,0 +1,32 @@ +namespace RnzTrauer.Core; + +/// +/// Provides an abstraction over for testable file-system access. +/// +public interface IFile +{ + /// + /// Determines whether the specified file exists. + /// + bool Exists(string sPath); + + /// + /// Reads all text from the specified file. + /// + string ReadAllText(string sPath); + + /// + /// Reads all bytes from the specified file. + /// + byte[] ReadAllBytes(string sPath); + + /// + /// Writes all text to the specified file. + /// + void WriteAllText(string sPath, string sContents); + + /// + /// Writes all bytes to the specified file. + /// + void WriteAllBytes(string sPath, byte[] arrBytes); +} diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/IHttpClientProxy.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/IHttpClientProxy.cs new file mode 100644 index 000000000..28ce45d2b --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/IHttpClientProxy.cs @@ -0,0 +1,12 @@ +namespace RnzTrauer.Core; + +/// +/// Provides an abstraction over for testable HTTP access. +/// +public interface IHttpClientProxy : IDisposable +{ + /// + /// Sends a GET request to the specified URI. + /// + Task GetAsync(string sRequestUri); +} diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/IWebDriverFactory.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/IWebDriverFactory.cs new file mode 100644 index 000000000..d4c8f49be --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/IWebDriverFactory.cs @@ -0,0 +1,14 @@ +using OpenQA.Selenium; + +namespace RnzTrauer.Core; + +/// +/// Creates Selenium web driver instances. +/// +public interface IWebDriverFactory +{ + /// + /// Creates a new web driver instance. + /// + IWebDriver Create(); +} diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/WebHandlerProgress.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/WebHandlerProgress.cs new file mode 100644 index 000000000..1044ea18b --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/WebHandlerProgress.cs @@ -0,0 +1,6 @@ +namespace RnzTrauer.Core; + +/// +/// Describes a progress update emitted by . +/// +public sealed record WebHandlerProgress(string Text, bool WriteLine = false); diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/ConfigLoaderTests.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/ConfigLoaderTests.cs new file mode 100644 index 000000000..1d6ef2f42 --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/ConfigLoaderTests.cs @@ -0,0 +1,136 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NSubstitute; +using RnzTrauer.Core; + +namespace RnzTrauer.Tests; + +[TestClass] +public sealed class ConfigLoaderTests +{ + [TestMethod] + public void Load_Uses_File_Abstraction_And_Deserializes_Content() + { + var xFile = Substitute.For(); + xFile.Exists("config.json").Returns(true); + xFile.ReadAllText("config.json").Returns( + """ + { + "name": "RNZ", + "count": 5 + } + """); + var xLoader = new ConfigLoader(xFile); + + var xResult = xLoader.Load("config.json"); + + Assert.IsNotNull(xResult); + Assert.AreEqual("RNZ", xResult.Name); + Assert.AreEqual(5, xResult.Count); + _ = xFile.Received(1).Exists("config.json"); + _ = xFile.Received(1).ReadAllText("config.json"); + } + + [TestMethod] + public void Load_Throws_When_File_Does_Not_Exist() + { + var xFile = Substitute.For(); + xFile.Exists("missing.json").Returns(false); + var xLoader = new ConfigLoader(xFile); + + try + { + _ = xLoader.Load("missing.json"); + Assert.Fail("Expected FileNotFoundException was not thrown."); + } + catch (FileNotFoundException xException) + { + StringAssert.Contains(xException.Message, "missing.json"); + } + } + + [TestMethod] + public void FileProxy_Delegates_To_File_Class() + { + var sPath = Path.GetTempFileName(); + var sTextPath = Path.ChangeExtension(sPath, ".txt"); + var sBytesPath = Path.ChangeExtension(sPath, ".bin"); + + try + { + File.WriteAllText(sPath, "proxy-test"); + var xProxy = new FileProxy(); + + Assert.IsTrue(xProxy.Exists(sPath)); + Assert.AreEqual("proxy-test", xProxy.ReadAllText(sPath)); + CollectionAssert.AreEqual(File.ReadAllBytes(sPath), xProxy.ReadAllBytes(sPath)); + + xProxy.WriteAllText(sTextPath, "written-text"); + xProxy.WriteAllBytes(sBytesPath, [1, 2, 3]); + + Assert.AreEqual("written-text", File.ReadAllText(sTextPath)); + CollectionAssert.AreEqual(new byte[] { 1, 2, 3 }, File.ReadAllBytes(sBytesPath)); + } + finally + { + if (File.Exists(sPath)) + { + File.Delete(sPath); + } + + if (File.Exists(sTextPath)) + { + File.Delete(sTextPath); + } + + if (File.Exists(sBytesPath)) + { + File.Delete(sBytesPath); + } + } + } + + [TestMethod] + public void RnzConfig_Load_Uses_Injected_ConfigLoader() + { + var xConfigLoader = Substitute.For(); + var xExpected = new RnzConfig + { + Url = "https://example.invalid", + Title = "RNZ" + }; + xConfigLoader.Load("rnz.json").Returns(xExpected); + + var xConfig = new RnzConfig(xConfigLoader); + + var xResult = xConfig.Load("rnz.json"); + + Assert.AreSame(xExpected, xResult); + _ = xConfigLoader.Received(1).Load("rnz.json"); + } + + [TestMethod] + public void AmtsblattConfig_Load_Uses_Injected_ConfigLoader() + { + var xConfigLoader = Substitute.For(); + var xExpected = new AmtsblattConfig + { + Url = "https://example.invalid", + Title = "Amtsblatt" + }; + xConfigLoader.Load("amtsblatt.json").Returns(xExpected); + + var xConfig = new AmtsblattConfig(xConfigLoader); + + var xResult = xConfig.Load("amtsblatt.json"); + + Assert.AreSame(xExpected, xResult); + _ = xConfigLoader.Received(1).Load("amtsblatt.json"); + } + + private sealed class TestConfig + { + public string Name { get; set; } = string.Empty; + + public int Count { get; set; } + } +} diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/PortedHelpersAdditionalTests.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/PortedHelpersAdditionalTests.cs new file mode 100644 index 000000000..b26e0890a --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/PortedHelpersAdditionalTests.cs @@ -0,0 +1,129 @@ +using System.Text.Json.Nodes; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using RnzTrauer.Core; + +namespace RnzTrauer.Tests; + +[TestClass] +public sealed class PortedHelpersAdditionalTests +{ + [TestMethod] + public void Str2Date_Returns_Date_For_Valid_Input() + { + var dtResult = PortedHelpers.Str2Date("24.12.2024"); + + Assert.AreEqual(new DateOnly(2024, 12, 24), dtResult); + } + + [DataTestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + [DataRow("24-12-2024")] + [DataRow("24.12")] + [DataRow("31.02.2024")] + [DataRow("aa.bb.cccc")] + public void Str2Date_Returns_Null_For_Invalid_Input(string? sValue) + { + Assert.IsNull(PortedHelpers.Str2Date(sValue)); + } + + [TestMethod] + public void Cond_Returns_Trimmed_String_For_Existing_Key() + { + IReadOnlyDictionary dValues = new Dictionary + { + ["name"] = " RNZ " + }; + + Assert.AreEqual("RNZ", dValues.Cond("name")); + } + + [TestMethod] + public void Cond_Returns_Empty_String_For_Missing_Or_Null_Value() + { + IReadOnlyDictionary dValues = new Dictionary + { + ["name"] = null + }; + + Assert.AreEqual(string.Empty, dValues.Cond("missing")); + Assert.AreEqual(string.Empty, dValues.Cond("name")); + } + + [DataTestMethod] + [DataRow("", "")] + [DataRow("a", "A")] + [DataRow("äPFEL", "Äpfel")] + [DataRow("rnZ", "Rnz")] + public void Capitalize_Normalizes_Text(string sValue, string sExpected) + { + Assert.AreEqual(sExpected, sValue.Capitalize()); + } + + [TestMethod] + public void ToJsonNode_Creates_Deep_Clone_For_JsonNode() + { + var xOriginal = JsonNode.Parse("""{"name":"RNZ"}""")!; + + var xClone = xOriginal.ToJsonNode(); + xClone!["name"] = "Changed"; + + Assert.AreEqual("RNZ", xOriginal["name"]!.ToString()); + Assert.AreEqual("Changed", xClone["name"]!.ToString()); + } + + [TestMethod] + public void ToJsonNode_Converts_DateOnly_And_ByteArray() + { + var xDateNode = new DateOnly(2024, 12, 24).ToJsonNode(); + var xBytesNode = new byte[] { 1, 2, 3 }.ToJsonNode(); + + Assert.AreEqual("2024-12-24", xDateNode!.ToString()); + Assert.AreEqual("AQID", xBytesNode!.ToString()); + } + + [TestMethod] + public void ToJsonNode_Converts_Dictionary_And_Array() + { + var xObjectNode = new Dictionary + { + ["name"] = "RNZ", + ["count"] = 2 + }.ToJsonNode(); + var xArrayNode = new object?[] { "RNZ", 2, true }.ToJsonNode(); + + Assert.IsInstanceOfType(xObjectNode); + Assert.IsInstanceOfType(xArrayNode); + Assert.AreEqual("RNZ", xObjectNode!["name"]!.ToString()); + Assert.AreEqual("2", xObjectNode["count"]!.ToString()); + Assert.AreEqual("RNZ", xArrayNode![0]!.ToString()); + Assert.AreEqual("2", xArrayNode[1]!.ToString()); + Assert.AreEqual("true", xArrayNode[2]!.ToString()); + } + + [TestMethod] + public void ToJsonObject_Converts_Dictionary_Entries() + { + IReadOnlyDictionary dValues = new Dictionary + { + ["name"] = "RNZ", + ["items"] = new object?[] { 1, "two" } + }; + + var xObject = dValues.ToJsonObject(); + + Assert.AreEqual("RNZ", xObject["name"]!.ToString()); + Assert.IsInstanceOfType(xObject["items"]); + Assert.AreEqual("1", xObject["items"]![0]!.ToString()); + Assert.AreEqual("two", xObject["items"]![1]!.ToString()); + } + + [TestMethod] + public void PdfText_Returns_Empty_String_For_Invalid_Pdf() + { + var sResult = PortedHelpers.PdfText([1, 2, 3, 4]); + + Assert.AreEqual(string.Empty, sResult); + } +} diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/WebHandlerTests.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/WebHandlerTests.cs new file mode 100644 index 000000000..0fb42f3cf --- /dev/null +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/WebHandlerTests.cs @@ -0,0 +1,458 @@ +using System.Collections.ObjectModel; +using System.Net; +using System.Net.Http.Headers; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NSubstitute; +using OpenQA.Selenium; +using RnzTrauer.Core; + +namespace RnzTrauer.Tests; + +[TestClass] +public sealed class WebHandlerTests +{ + [TestMethod] + public void InitPage_Uses_Injected_DriverFactory_And_Submits_Login() + { + var xConfig = new RnzConfig + { + Url = "https://example.invalid/login", + User = "user@example.invalid", + Password = "secret", + Title = "RNZ" + }; + var xHttpClient = Substitute.For(); + var xWebDriverFactory = Substitute.For(); + var xDriver = Substitute.For(); + var xNavigation = Substitute.For(); + var xEmailElement = Substitute.For(); + var xPasswordElement = Substitute.For(); + var xFormElement = Substitute.For(); + + xDriver.Navigate().Returns(xNavigation); + xDriver.Title.Returns("Loaded"); + xDriver.FindElement(Arg.Is(xBy => xBy.ToString() == By.Id("emailAddress").ToString())).Returns(xEmailElement); + xDriver.FindElement(Arg.Is(xBy => xBy.ToString() == By.Id("password").ToString())).Returns(xPasswordElement); + xDriver.FindElement(Arg.Is(xBy => xBy.ToString() == By.Id("form").ToString())).Returns(xFormElement); + xWebDriverFactory.Create().Returns(xDriver); + + using var xHandler = new WebHandler(xConfig, xHttpClient, xWebDriverFactory); + + xHandler.InitPage(); + + _ = xWebDriverFactory.Received(1).Create(); + xNavigation.Received(1).GoToUrl(xConfig.Url); + xEmailElement.Received(1).SendKeys(xConfig.User); + xPasswordElement.Received(1).SendKeys(xConfig.Password); + xFormElement.Received(1).Submit(); + Assert.AreSame(xDriver, xHandler.Driver); + } + + [TestMethod] + public void Wdr2List_Reads_Attributes_Text_And_Children() + { + var xConfig = new RnzConfig(); + var xHttpClient = Substitute.For(); + var xWebDriverFactory = Substitute.For(); + var xSearchContext = Substitute.For(); + var xParentElement = Substitute.For(); + var xChildElement = Substitute.For(); + + xSearchContext.FindElements(Arg.Is(xBy => xBy.ToString() == By.TagName("div").ToString())) + .Returns(CreateCollection(xParentElement)); + xParentElement.TagName.Returns("div"); + xParentElement.Text.Returns($"Line1{Environment.NewLine}Line2"); + xParentElement.GetAttribute("class").Returns("c-blockitem"); + xParentElement.GetAttribute("id").Returns(_ => throw new InvalidOperationException()); + xParentElement.FindElements(Arg.Is(xBy => xBy.ToString() == By.TagName("a").ToString())) + .Returns(CreateCollection(xChildElement)); + + xChildElement.TagName.Returns("a"); + xChildElement.Text.Returns("Read more"); + xChildElement.GetAttribute("title").Returns("title"); + xChildElement.GetAttribute("target").Returns("_blank"); + xChildElement.GetAttribute("href").Returns("https://example.invalid/item"); + xChildElement.FindElements(Arg.Any()).Returns(CreateCollection()); + + using var xHandler = new WebHandler(xConfig, xHttpClient, xWebDriverFactory); + + var lstResult = xHandler.Wdr2List(xSearchContext, new WebQuery(string.Empty, By.TagName("div"), new WebQuery("links", By.TagName("a")))); + + Assert.AreEqual(1, lstResult.Count); + Assert.AreEqual("div", lstResult[0]["tag"]); + Assert.AreEqual("c-blockitem", lstResult[0]["class"]); + Assert.AreEqual(string.Empty, lstResult[0]["id"]); + CollectionAssert.AreEqual(new object?[] { "Line1", "Line2" }, (System.Collections.ICollection)lstResult[0]["text"]!); + var lstLinks = (List>)lstResult[0]["links"]!; + Assert.AreEqual(1, lstLinks.Count); + Assert.AreEqual("a", lstLinks[0]["tag"]); + Assert.AreEqual("https://example.invalid/item", lstLinks[0]["href"]); + } + + [TestMethod] + public void GetData1_Throws_When_Driver_Has_Not_Been_Initialized() + { + using var xHandler = new WebHandler(new RnzConfig(), Substitute.For(), Substitute.For()); + + try + { + _ = xHandler.GetData1("https://example.invalid/start"); + Assert.Fail("Expected InvalidOperationException was not thrown."); + } + catch (InvalidOperationException xException) + { + StringAssert.Contains(xException.Message, "initialized"); + } + } + + [TestMethod] + public void GetData1_Loads_Announcement_Media_Using_Injected_Dependencies() + { + var xConfig = new RnzConfig + { + Url = "https://example.invalid/login", + User = "user@example.invalid", + Password = "secret", + Title = "RNZ" + }; + var xHttpClient = Substitute.For(); + var xWebDriverFactory = Substitute.For(); + var xProgress = Substitute.For>(); + var xDriver = Substitute.For(); + var xNavigation = Substitute.For(); + var xEmailElement = Substitute.For(); + var xPasswordElement = Substitute.For(); + var xFormElement = Substitute.For(); + var xAnnouncementElement = Substitute.For(); + var xImageElement = Substitute.For(); + var sCurrentUrl = string.Empty; + + xNavigation.When(xNav => xNav.GoToUrl(Arg.Any())) + .Do(xCall => sCurrentUrl = xCall.Arg()); + xDriver.Navigate().Returns(xNavigation); + xDriver.Url.Returns(_ => sCurrentUrl); + xDriver.Title.Returns("Loaded"); + xDriver.FindElement(Arg.Is(xBy => xBy.ToString() == By.Id("emailAddress").ToString())).Returns(xEmailElement); + xDriver.FindElement(Arg.Is(xBy => xBy.ToString() == By.Id("password").ToString())).Returns(xPasswordElement); + xDriver.FindElement(Arg.Is(xBy => xBy.ToString() == By.Id("form").ToString())).Returns(xFormElement); + xDriver.FindElements(Arg.Any()).Returns(xCall => + { + var xBy = xCall.Arg(); + if (xBy.ToString() == By.ClassName("c-blockitem").ToString()) + { + return CreateCollection(xAnnouncementElement); + } + + return CreateCollection(); + }); + xWebDriverFactory.Create().Returns(xDriver); + + xAnnouncementElement.TagName.Returns("div"); + xAnnouncementElement.Text.Returns($"ANZ 12345678{Environment.NewLine}Info"); + xAnnouncementElement.GetAttribute("class").Returns("c-blockitem"); + xAnnouncementElement.GetAttribute("id").Returns("item-1"); + xAnnouncementElement.FindElements(Arg.Is(xBy => xBy.ToString() == By.TagName("a").ToString())).Returns(CreateCollection()); + xAnnouncementElement.FindElements(Arg.Is(xBy => xBy.ToString() == By.TagName("img").ToString())).Returns(CreateCollection(xImageElement)); + + xImageElement.TagName.Returns("img"); + xImageElement.Text.Returns(string.Empty); + xImageElement.GetAttribute("class").Returns(string.Empty); + xImageElement.GetAttribute("title").Returns(string.Empty); + xImageElement.GetAttribute("alt").Returns("image alt"); + xImageElement.GetAttribute("src").Returns("https://trauer.rnz.de/MEDIASERVER/image.jpg"); + xImageElement.GetAttribute("style").Returns(string.Empty); + xImageElement.GetAttribute("data-original").Returns(string.Empty); + xImageElement.FindElements(Arg.Any()).Returns(CreateCollection()); + + var xResponse = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new ByteArrayContent(Encoding.ASCII.GetBytes("PNG1234567")) + }; + xResponse.Headers.Add("X-Test", "1"); + xResponse.Content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); + xHttpClient.GetAsync("https://trauer.rnz.de/MEDIASERVER/image.jpg").Returns(Task.FromResult(xResponse)); + + using var xHandler = new WebHandler(xConfig, xHttpClient, xWebDriverFactory, xProgress); + xHandler.InitPage(); + + var (dPages, lstItems) = xHandler.GetData1("https://example.invalid/start", 1); + + Assert.AreEqual(4, dPages.Count); + Assert.AreEqual(4, lstItems.Count); + Assert.IsTrue(lstItems.All(dItem => dItem.ContainsKey(WebHandler.CsData))); + Assert.IsTrue(lstItems.All(dItem => dItem.ContainsKey(WebHandler.CsHeader))); + Assert.IsTrue(dPages.Values.All(dPage => dPage.ContainsKey("https://trauer.rnz.de/MEDIASERVER/image.jpg"))); + _ = xHttpClient.Received(4).GetAsync("https://trauer.rnz.de/MEDIASERVER/image.jpg"); + xProgress.Received().Report(new WebHandlerProgress(".")); + } + + [TestMethod] + public void TryLoadBinary_Loads_Data_And_Headers_Via_Reflection() + { + var xHttpClient = Substitute.For(); + var xResponse = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new ByteArrayContent([1, 2, 3]) + }; + xResponse.Headers.Add("X-Test", "1"); + xResponse.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + xHttpClient.GetAsync("https://example.invalid/data.bin").Returns(Task.FromResult(xResponse)); + using var xHandler = CreateHandler(xHttpClient: xHttpClient); + var dTarget = new Dictionary(); + + InvokeNonPublicInstanceMethod(xHandler, "TryLoadBinary", "https://example.invalid/data.bin", dTarget); + + CollectionAssert.AreEqual(new byte[] { 1, 2, 3 }, (byte[])dTarget[WebHandler.CsData]!); + Assert.IsInstanceOfType>(dTarget[WebHandler.CsHeader]); + var dHeaders = (Dictionary)dTarget[WebHandler.CsHeader]!; + Assert.AreEqual("1", dHeaders["X-Test"]); + } + + [TestMethod] + public void TryLoadBinary_Sets_Empty_Result_On_Failure_Via_Reflection() + { + var xHttpClient = Substitute.For(); + xHttpClient.GetAsync("https://example.invalid/data.bin").Returns>(_ => throw new HttpRequestException("failed")); + using var xHandler = CreateHandler(xHttpClient: xHttpClient); + var dTarget = new Dictionary(); + + InvokeNonPublicInstanceMethod(xHandler, "TryLoadBinary", "https://example.invalid/data.bin", dTarget); + + CollectionAssert.AreEqual(Array.Empty(), (byte[])dTarget[WebHandler.CsData]!); + Assert.IsInstanceOfType>(dTarget[WebHandler.CsHeader]); + Assert.AreEqual(0, ((Dictionary)dTarget[WebHandler.CsHeader]!).Count); + } + + [TestMethod] + public void GetDictionaryList_Returns_Private_List_Result_Via_Reflection() + { + IReadOnlyDictionary dValues = new Dictionary + { + ["links"] = new List>() + { + new(StringComparer.Ordinal) { ["href"] = "https://example.invalid" } + } + }; + + var lstResult = InvokeNonPublicStaticMethod>>(typeof(WebHandler), "GetDictionaryList", dValues, "links"); + var lstEmpty = InvokeNonPublicStaticMethod>>(typeof(WebHandler), "GetDictionaryList", dValues, "missing"); + + Assert.AreEqual(1, lstResult.Count); + Assert.AreEqual("https://example.invalid", lstResult[0]["href"]); + Assert.AreEqual(0, lstEmpty.Count); + } + + [TestMethod] + public void GetStringList_Returns_Private_List_Result_Via_Reflection() + { + IReadOnlyDictionary dValues = new Dictionary + { + ["text"] = new List { "Line1", 2, null } + }; + + var lstResult = InvokeNonPublicStaticMethod>(typeof(WebHandler), "GetStringList", dValues, "text"); + var lstEmpty = InvokeNonPublicStaticMethod>(typeof(WebHandler), "GetStringList", dValues, "missing"); + + CollectionAssert.AreEqual(new[] { "Line1", "2", string.Empty }, lstResult); + Assert.AreEqual(0, lstEmpty.Count); + } + + [TestMethod] + public void WorkMainPage_Returns_Page_SubPages_And_NextUrl_Via_Reflection() + { + var xDriver = Substitute.For(); + var xAnnouncementElement = Substitute.For(); + var xAnnouncementLink = Substitute.For(); + var xPagerElement = Substitute.For(); + var xNextLink = Substitute.For(); + using var xHandler = CreateHandler(); + SetDriver(xHandler, xDriver); + + xDriver.Url.Returns("https://example.invalid/list"); + xDriver.Title.Returns("Loaded"); + xDriver.PageSource.Returns(""); + xDriver.FindElements(Arg.Is(xBy => xBy.ToString() == By.ClassName("c-blockitem").ToString())) + .Returns(CreateCollection(xAnnouncementElement, xPagerElement)); + + xAnnouncementElement.TagName.Returns("div"); + xAnnouncementElement.Text.Returns($"ANZ 12345678{Environment.NewLine}Info"); + xAnnouncementElement.GetAttribute("class").Returns("c-blockitem"); + xAnnouncementElement.GetAttribute("id").Returns("ann-1"); + xAnnouncementElement.FindElements(Arg.Is(xBy => xBy.ToString() == By.TagName("a").ToString())).Returns(CreateCollection(xAnnouncementLink)); + xAnnouncementElement.FindElements(Arg.Is(xBy => xBy.ToString() == By.TagName("img").ToString())).Returns(CreateCollection()); + + xAnnouncementLink.TagName.Returns("a"); + xAnnouncementLink.Text.Returns(string.Empty); + xAnnouncementLink.GetAttribute("title").Returns(string.Empty); + xAnnouncementLink.GetAttribute("target").Returns(string.Empty); + xAnnouncementLink.GetAttribute("href").Returns("https://example.invalid/subpage"); + xAnnouncementLink.FindElements(Arg.Any()).Returns(CreateCollection()); + + xPagerElement.TagName.Returns("div"); + xPagerElement.Text.Returns($"Page{Environment.NewLine}>\n"); + xPagerElement.GetAttribute("class").Returns("c-blockitem"); + xPagerElement.GetAttribute("id").Returns("pager-1"); + xPagerElement.FindElements(Arg.Is(xBy => xBy.ToString() == By.TagName("a").ToString())).Returns(CreateCollection(xNextLink)); + xPagerElement.FindElements(Arg.Is(xBy => xBy.ToString() == By.TagName("img").ToString())).Returns(CreateCollection()); + + xNextLink.TagName.Returns("a"); + xNextLink.Text.Returns(">"); + xNextLink.GetAttribute("title").Returns(string.Empty); + xNextLink.GetAttribute("target").Returns(string.Empty); + xNextLink.GetAttribute("href").Returns("https://example.invalid/list?page=2"); + xNextLink.FindElements(Arg.Any()).Returns(CreateCollection()); + + var dPages = new Dictionary>(StringComparer.Ordinal); + var lstItems = new List>(); + + var tResult = InvokeNonPublicInstanceMethod<(List SubPages, string Url, string NextUrl)>(xHandler, "WorkMainPage", dPages, lstItems, "nachrufe"); + + Assert.AreEqual("https://example.invalid/list", tResult.Url); + Assert.AreEqual("https://example.invalid/list?page=2", tResult.NextUrl); + CollectionAssert.AreEqual(new[] { "https://example.invalid/subpage" }, tResult.SubPages); + Assert.AreEqual(1, lstItems.Count); + Assert.AreEqual("5678", lstItems[0]["Title"]); + Assert.AreEqual("Info", lstItems[0]["Info"]); + Assert.AreEqual("nachrufe", dPages[tResult.Url]["filter"]); + } + + [TestMethod] + public void WorkSubPage_Extracts_Metadata_And_Media_Via_Reflection() + { + var xHttpClient = Substitute.For(); + var xDriver = Substitute.For(); + var xTitleElement = Substitute.For(); + var xBirthElement = Substitute.For(); + var xDeathElement = Substitute.For(); + var xInfoSection = Substitute.For(); + var xMediaImage = Substitute.For(); + var xResponse = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new ByteArrayContent(Encoding.ASCII.GetBytes("PNG1234567")) + }; + xHttpClient.GetAsync("https://trauer.rnz.de/MEDIASERVER/profile.jpg").Returns(Task.FromResult(xResponse)); + using var xHandler = CreateHandler(xHttpClient: xHttpClient); + SetDriver(xHandler, xDriver); + + xDriver.Url.Returns("https://example.invalid/subpage/anzeigen"); + xDriver.Title.Returns("SubPage"); + xDriver.PageSource.Returns("sub"); + xDriver.FindElements(Arg.Is(xBy => xBy.ToString() == By.TagName("h1").ToString())).Returns(CreateCollection(xTitleElement)); + xDriver.FindElements(Arg.Is(xBy => xBy.ToString() == By.ClassName("col-sm-6").ToString())).Returns(CreateCollection(xBirthElement, xDeathElement)); + xDriver.FindElements(Arg.Is(xBy => xBy.ToString() == By.TagName("section").ToString())).Returns(CreateCollection(xInfoSection)); + + xTitleElement.Text.Returns("Max Mustermann"); + xBirthElement.Text.Returns("* 01.01.2000"); + xDeathElement.Text.Returns("† 02.02.2020in Heidelberg"); + + xInfoSection.TagName.Returns("section"); + xInfoSection.Text.Returns($"Erstellt von Test{Environment.NewLine}Angelegt am Heute{Environment.NewLine}42 Besuche"); + xInfoSection.GetAttribute("class").Returns("col-12"); + xInfoSection.GetAttribute("id").Returns("sec-1"); + xInfoSection.FindElements(Arg.Is(xBy => xBy.ToString() == By.TagName("a").ToString())).Returns(CreateCollection()); + xInfoSection.FindElements(Arg.Is(xBy => xBy.ToString() == By.TagName("img").ToString())).Returns(CreateCollection(xMediaImage)); + + xMediaImage.TagName.Returns("img"); + xMediaImage.Text.Returns(string.Empty); + xMediaImage.GetAttribute("class").Returns(string.Empty); + xMediaImage.GetAttribute("title").Returns(string.Empty); + xMediaImage.GetAttribute("alt").Returns("Profile image"); + xMediaImage.GetAttribute("src").Returns("https://trauer.rnz.de/MEDIASERVER/profile.jpg"); + xMediaImage.GetAttribute("style").Returns(string.Empty); + xMediaImage.GetAttribute("data-original").Returns(string.Empty); + xMediaImage.FindElements(Arg.Any()).Returns(CreateCollection()); + + var dPages = new Dictionary>(StringComparer.Ordinal) + { + ["https://example.invalid/list"] = new Dictionary(StringComparer.Ordinal) + { + ["filter"] = "nachrufe", + ["sections"] = new List>() + { + new(StringComparer.Ordinal) + { + ["links"] = new List>() + { + new(StringComparer.Ordinal) { ["href"] = "https://example.invalid/subpage" } + }, + ["imgs"] = new List>() + } + } + } + }; + var lstItems = new List>(); + + InvokeNonPublicInstanceMethod(xHandler, "WorkSubPage", "https://example.invalid/list", dPages, lstItems); + + var dSubPage = dPages["https://example.invalid/subpage/anzeigen"]; + Assert.AreEqual("SubPage", dSubPage["Title"]); + Assert.AreEqual("Max Mustermann", dSubPage["name"]); + Assert.AreEqual("* 01.01.2000", dSubPage["Birth"]); + Assert.AreEqual("† 02.02.2020", dSubPage["Death"]); + Assert.AreEqual(" Heidelberg", dSubPage["Place"]); + Assert.AreEqual(" Test", dSubPage["created_by"]); + Assert.AreEqual(" Heute", dSubPage["created_on"]); + Assert.AreEqual("42 ", dSubPage["visits"]); + Assert.AreEqual(1, lstItems.Count); + Assert.AreEqual("Profile image", lstItems[0]["Info"]); + Assert.AreEqual("https://example.invalid/subpage/anzeigen", lstItems[0]["parent"]); + } + + private static ReadOnlyCollection CreateCollection(params IWebElement[] arrElements) + { + return new ReadOnlyCollection(arrElements.ToList()); + } + + private static WebHandler CreateHandler(IHttpClientProxy? xHttpClient = null, IWebDriverFactory? xWebDriverFactory = null, IProgress? xProgress = null) + { + return new WebHandler(new RnzConfig(), xHttpClient ?? Substitute.For(), xWebDriverFactory ?? Substitute.For(), xProgress); + } + + private static void SetDriver(WebHandler xHandler, IWebDriver xDriver) + { + var xSetter = typeof(WebHandler).GetProperty(nameof(WebHandler.Driver), BindingFlags.Instance | BindingFlags.Public)?.GetSetMethod(true); + Assert.IsNotNull(xSetter); + xSetter.Invoke(xHandler, [xDriver]); + } + + private static void InvokeNonPublicInstanceMethod(object xTarget, string sMethodName, params object?[] arrArguments) + { + var xMethod = xTarget.GetType().GetMethod(sMethodName, BindingFlags.Instance | BindingFlags.NonPublic); + Assert.IsNotNull(xMethod); + + try + { + _ = xMethod.Invoke(xTarget, arrArguments); + } + catch (TargetInvocationException xException) when (xException.InnerException is not null) + { + ExceptionDispatchInfo.Capture(xException.InnerException).Throw(); + } + } + + private static T InvokeNonPublicInstanceMethod(object xTarget, string sMethodName, params object?[] arrArguments) + { + var xMethod = xTarget.GetType().GetMethod(sMethodName, BindingFlags.Instance | BindingFlags.NonPublic); + Assert.IsNotNull(xMethod); + + try + { + return (T)xMethod.Invoke(xTarget, arrArguments)!; + } + catch (TargetInvocationException xException) when (xException.InnerException is not null) + { + ExceptionDispatchInfo.Capture(xException.InnerException).Throw(); + throw; + } + } + + private static T InvokeNonPublicStaticMethod(Type xType, string sMethodName, params object?[] arrArguments) + { + var xMethod = xType.GetMethod(sMethodName, BindingFlags.Static | BindingFlags.NonPublic); + Assert.IsNotNull(xMethod); + return (T)xMethod.Invoke(null, arrArguments)!; + } +} From 38c47d11505f3f8991d27b0d95a7d6774c7fea32 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 02:39:38 +0200 Subject: [PATCH 26/96] GenFreeBase --- GenFreeWin/GenFreeBase/Interfaces/Sys/IModul1.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/GenFreeWin/GenFreeBase/Interfaces/Sys/IModul1.cs b/GenFreeWin/GenFreeBase/Interfaces/Sys/IModul1.cs index 5997735f0..3f778972c 100644 --- a/GenFreeWin/GenFreeBase/Interfaces/Sys/IModul1.cs +++ b/GenFreeWin/GenFreeBase/Interfaces/Sys/IModul1.cs @@ -392,7 +392,6 @@ public struct Letzter int PersSp { get; set; } string Datschuname { get; set; } string Eltq { get; set; } - string Ubg1T { get; set; } void Datles2(); string Person_FullSurname(IPersonData person, bool xFamToUpper); From 603ae2751d99fd1138b200f97d53199646f217d7 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 02:39:47 +0200 Subject: [PATCH 27/96] AmtsblattLoader.Console --- .../AmtsblattLoader.Console.csproj | 1 + .../RnzTrauer/AmtsblattLoader.Console/Program.cs | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/WinAhnenNew/RnzTrauer/AmtsblattLoader.Console/AmtsblattLoader.Console.csproj b/WinAhnenNew/RnzTrauer/AmtsblattLoader.Console/AmtsblattLoader.Console.csproj index 81f4a1607..a078f0b34 100644 --- a/WinAhnenNew/RnzTrauer/AmtsblattLoader.Console/AmtsblattLoader.Console.csproj +++ b/WinAhnenNew/RnzTrauer/AmtsblattLoader.Console/AmtsblattLoader.Console.csproj @@ -5,6 +5,7 @@ + diff --git a/WinAhnenNew/RnzTrauer/AmtsblattLoader.Console/Program.cs b/WinAhnenNew/RnzTrauer/AmtsblattLoader.Console/Program.cs index a0d35639d..b533b3436 100644 --- a/WinAhnenNew/RnzTrauer/AmtsblattLoader.Console/Program.cs +++ b/WinAhnenNew/RnzTrauer/AmtsblattLoader.Console/Program.cs @@ -1,13 +1,21 @@ -using AmtsblattLoader.Console.ViewModels; +using Microsoft.Extensions.DependencyInjection; +using AmtsblattLoader.Console.ViewModels; using AmtsblattLoader.Console.Views; using RnzTrauer.Core; -var xView = new ConsoleOutputView(); +var xServices = new ServiceCollection() + .AddSingleton() + .AddSingleton() + .AddTransient() + .AddTransient() + .BuildServiceProvider(); + +var xView = xServices.GetRequiredService(); try { - var xConfig = AmtsblattConfig.Load(Path.Combine(AppContext.BaseDirectory, "Amtsblatt_Cfg.json")); - var xViewModel = new AmtsblattLoaderConsoleViewModel(xView); + var xConfig = new AmtsblattConfig(xServices.GetRequiredService()).Load(Path.Combine(AppContext.BaseDirectory, "Amtsblatt_Cfg.json")); + var xViewModel = xServices.GetRequiredService(); xViewModel.Run(xConfig); } catch (FileNotFoundException ex) From 83da0e89491ffda75ca77272665f0e053b9a076e Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 02:39:48 +0200 Subject: [PATCH 28/96] RnzTrauer.Console --- .../RnzTrauer/RnzTrauer.Console/Program.cs | 18 ++++-- .../RnzTrauer.Console.csproj | 1 + .../ViewModels/RnzTrauerConsoleViewModel.cs | 55 ++++++++++++------- 3 files changed, 51 insertions(+), 23 deletions(-) diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Console/Program.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Console/Program.cs index 1edeba79c..c1471efa9 100644 --- a/WinAhnenNew/RnzTrauer/RnzTrauer.Console/Program.cs +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Console/Program.cs @@ -1,13 +1,23 @@ -using RnzTrauer.Console.ViewModels; +using Microsoft.Extensions.DependencyInjection; +using RnzTrauer.Console.ViewModels; using RnzTrauer.Console.Views; using RnzTrauer.Core; -var xView = new ConsoleOutputView(); +var xServices = new ServiceCollection() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddTransient() + .AddTransient() + .BuildServiceProvider(); + +var xView = xServices.GetRequiredService(); try { - var xConfig = RnzConfig.Load(Path.Combine(AppContext.BaseDirectory, "RNZ_Config.json")); - var xViewModel = new RnzTrauerConsoleViewModel(xView); + var xConfig = new RnzConfig(xServices.GetRequiredService()).Load(Path.Combine(AppContext.BaseDirectory, "RNZ_Config.json")); + var xViewModel = xServices.GetRequiredService(); xViewModel.Run(xConfig); } catch (FileNotFoundException ex) diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Console/RnzTrauer.Console.csproj b/WinAhnenNew/RnzTrauer/RnzTrauer.Console/RnzTrauer.Console.csproj index 4d005f321..63abeaa39 100644 --- a/WinAhnenNew/RnzTrauer/RnzTrauer.Console/RnzTrauer.Console.csproj +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Console/RnzTrauer.Console.csproj @@ -5,6 +5,7 @@ + diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Console/ViewModels/RnzTrauerConsoleViewModel.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Console/ViewModels/RnzTrauerConsoleViewModel.cs index def394184..2513bec30 100644 --- a/WinAhnenNew/RnzTrauer/RnzTrauer.Console/ViewModels/RnzTrauerConsoleViewModel.cs +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Console/ViewModels/RnzTrauerConsoleViewModel.cs @@ -11,13 +11,19 @@ namespace RnzTrauer.Console.ViewModels; public sealed class RnzTrauerConsoleViewModel { private readonly ConsoleOutputView _view; + private readonly IFile _xFile; + private readonly IHttpClientProxy _xHttpClient; + private readonly IWebDriverFactory _xWebDriverFactory; /// /// Initializes a new instance of the class. /// - public RnzTrauerConsoleViewModel(ConsoleOutputView xView) + public RnzTrauerConsoleViewModel(ConsoleOutputView xView, IFile xFile, IHttpClientProxy xHttpClient, IWebDriverFactory xWebDriverFactory) { _view = xView; + _xFile = xFile ?? throw new ArgumentNullException(nameof(xFile)); + _xHttpClient = xHttpClient ?? throw new ArgumentNullException(nameof(xHttpClient)); + _xWebDriverFactory = xWebDriverFactory ?? throw new ArgumentNullException(nameof(xWebDriverFactory)); } /// @@ -26,7 +32,18 @@ public RnzTrauerConsoleViewModel(ConsoleOutputView xView) public void Run(RnzConfig xConfig) { _view.WriteLine("Start..."); - using var xWebHandler = new WebHandler(xConfig); + var xProgress = new Progress(xUpdate => + { + if (xUpdate.WriteLine) + { + _view.WriteLine(xUpdate.Text); + } + else + { + _view.Write(xUpdate.Text); + } + }); + using var xWebHandler = new WebHandler(xConfig, _xHttpClient, _xWebDriverFactory, xProgress); xWebHandler.InitPage(); _view.WriteLine("Init..."); @@ -86,7 +103,7 @@ public void Run(RnzConfig xConfig) xWebHandler.Close(); - using var xDataHandler = new DataHandler(xConfig); + using var xDataHandler = new DataHandler(xConfig, _xFile); iDayDelta = 0; while (iDayDelta <= 14) { @@ -108,10 +125,10 @@ public void Run(RnzConfig xConfig) var sJsonFile = Path.HasExtension(sPath) ? Path.ChangeExtension(sPath, ".json") : sPath + ".json"; - if (File.Exists(sJsonFile)) + if (_xFile.Exists(sJsonFile)) { - var xData = JsonNode.Parse(File.ReadAllText(sJsonFile)); - var arrTrauerfaelle = DataHandler.ExtractTrauerData(xData, xConfig.LocalPath); + var xData = JsonNode.Parse(_xFile.ReadAllText(sJsonFile)); + var arrTrauerfaelle = xDataHandler.ExtractTrauerData(xData, xConfig.LocalPath); xDataHandler.TrauerDataToDb(arrTrauerfaelle, xConfig.LocalPath); } else @@ -143,11 +160,11 @@ private void SavePages(RnzConfig xConfig, Dictionary arrParentList) { @@ -214,7 +231,7 @@ private void SavePages(RnzConfig xConfig, Dictionary> arrI sParentPath = Path.HasExtension(sParentPath) ? Path.ChangeExtension(sParentPath, ".json") : sParentPath + ".json"; JsonObject xParentData; var xParentChanged = false; - if (File.Exists(sParentPath)) + if (_xFile.Exists(sParentPath)) { try { - xParentData = JsonNode.Parse(File.ReadAllText(sParentPath)) as JsonObject ?? new JsonObject(); + xParentData = JsonNode.Parse(_xFile.ReadAllText(sParentPath)) as JsonObject ?? new JsonObject(); } catch { @@ -274,7 +291,7 @@ private void SaveMedia(RnzConfig xConfig, List> arrI var sLocalPath = PortedHelpers.GetLocalPath(sHref, xConfig.LocalPath, dtCurrent); var sDataPath = Path.Combine(sLocalPath, $"data_{dtCurrent:yyyy-MM-dd}.json"); Directory.CreateDirectory(Path.GetDirectoryName(sDataPath)!); - File.WriteAllText(sDataPath, PortedHelpers.ToJsonObject(dEntryCopy).ToJsonString(PortedHelpers.JsonOptions)); + _xFile.WriteAllText(sDataPath, PortedHelpers.ToJsonObject(dEntryCopy).ToJsonString(PortedHelpers.JsonOptions)); } var sSource = Convert.ToString(dEntry.GetValueOrDefault(WebHandler.CsSrc)) ?? string.Empty; @@ -301,7 +318,7 @@ private void SaveMedia(RnzConfig xConfig, List> arrI } Directory.CreateDirectory(Path.GetDirectoryName(sFilePath)!); - File.WriteAllBytes(sFilePath, arrData); + _xFile.WriteAllBytes(sFilePath, arrData); dEntryCopy["localpath"] = sFilePath; xParentData[sSource] = PortedHelpers.ToJsonObject(dEntryCopy); xParentChanged = true; @@ -310,7 +327,7 @@ private void SaveMedia(RnzConfig xConfig, List> arrI if (xParentChanged) { Directory.CreateDirectory(Path.GetDirectoryName(sParentPath)!); - File.WriteAllText(sParentPath, xParentData.ToJsonString(PortedHelpers.JsonOptions)); + _xFile.WriteAllText(sParentPath, xParentData.ToJsonString(PortedHelpers.JsonOptions)); } } catch From db110fcb2de2d54414daa5257f11dcb18f2ca4dc Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 02:39:48 +0200 Subject: [PATCH 29/96] RnzTrauer.Core --- .../RnzTrauer.Core/Models/AmtsblattConfig.cs | 21 +++++- .../RnzTrauer.Core/Models/RnzConfig.cs | 21 +++++- .../RnzTrauer.Core/Services/ConfigLoader.cs | 18 +++-- .../RnzTrauer.Core/Services/DataHandler.cs | 65 ++++++++++--------- .../RnzTrauer.Core/Services/PortedHelpers.cs | 64 +++++++++--------- .../RnzTrauer.Core/Services/WebHandler.cs | 35 +++++----- 6 files changed, 134 insertions(+), 90 deletions(-) diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Models/AmtsblattConfig.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Models/AmtsblattConfig.cs index af1064011..c250114f8 100644 --- a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Models/AmtsblattConfig.cs +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Models/AmtsblattConfig.cs @@ -5,6 +5,23 @@ namespace RnzTrauer.Core; /// public sealed class AmtsblattConfig : DatabaseSettings { + private readonly IConfigLoader? _xConfigLoader; + + /// + /// Initializes a new instance of the class. + /// + public AmtsblattConfig() + { + } + + /// + /// Initializes a new instance of the class with a configuration loader dependency. + /// + public AmtsblattConfig(IConfigLoader xConfigLoader) + { + _xConfigLoader = xConfigLoader ?? throw new ArgumentNullException(nameof(xConfigLoader)); + } + /// /// Gets or sets the page URL prefix. /// @@ -23,8 +40,8 @@ public sealed class AmtsblattConfig : DatabaseSettings /// /// Loads the configuration from a JSON file. /// - public static AmtsblattConfig Load(string sFilePath) + public AmtsblattConfig Load(string sFilePath) { - return ConfigLoader.Load(sFilePath); + return (_xConfigLoader ?? throw new InvalidOperationException("No configuration loader has been provided.")).Load(sFilePath); } } diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Models/RnzConfig.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Models/RnzConfig.cs index a84fa0cae..ed4d0709e 100644 --- a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Models/RnzConfig.cs +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Models/RnzConfig.cs @@ -5,6 +5,23 @@ namespace RnzTrauer.Core; /// public sealed class RnzConfig : DatabaseSettings { + private readonly IConfigLoader? _xConfigLoader; + + /// + /// Initializes a new instance of the class. + /// + public RnzConfig() + { + } + + /// + /// Initializes a new instance of the class with a configuration loader dependency. + /// + public RnzConfig(IConfigLoader xConfigLoader) + { + _xConfigLoader = xConfigLoader ?? throw new ArgumentNullException(nameof(xConfigLoader)); + } + /// /// Gets or sets the login URL. /// @@ -33,8 +50,8 @@ public sealed class RnzConfig : DatabaseSettings /// /// Loads the configuration from a JSON file. /// - public static RnzConfig Load(string sFilePath) + public RnzConfig Load(string sFilePath) { - return ConfigLoader.Load(sFilePath); + return (_xConfigLoader ?? throw new InvalidOperationException("No configuration loader has been provided.")).Load(sFilePath); } } diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/ConfigLoader.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/ConfigLoader.cs index c86ced232..d18080b42 100644 --- a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/ConfigLoader.cs +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/ConfigLoader.cs @@ -5,7 +5,7 @@ namespace RnzTrauer.Core; /// /// Provides JSON-based configuration loading for the ported tools. /// -public static class ConfigLoader +public sealed class ConfigLoader : IConfigLoader { private static readonly JsonSerializerOptions _options = new() { @@ -13,17 +13,27 @@ public static class ConfigLoader WriteIndented = true }; + private readonly IFile _xFile; + + /// + /// Initializes a new instance of the class. + /// + public ConfigLoader(IFile xFile) + { + _xFile = xFile ?? throw new ArgumentNullException(nameof(xFile)); + } + /// /// Loads a configuration instance from the specified JSON file. /// - public static T Load(string sFilePath) where T : new() + public T Load(string sFilePath) where T : new() { - if (!File.Exists(sFilePath)) + if (!_xFile.Exists(sFilePath)) { throw new FileNotFoundException($"Configuration file was not found: {sFilePath}"); } - var xConfiguration = JsonSerializer.Deserialize(File.ReadAllText(sFilePath), _options); + var xConfiguration = JsonSerializer.Deserialize(_xFile.ReadAllText(sFilePath), _options); return xConfiguration ?? new T(); } } diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/DataHandler.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/DataHandler.cs index 315a08274..d2814125f 100644 --- a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/DataHandler.cs +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/DataHandler.cs @@ -11,12 +11,15 @@ namespace RnzTrauer.Core; public sealed class DataHandler : IDisposable { private readonly MySqlConnection _dbConn; + private readonly IFile _xFile; /// /// Initializes a new instance of the class. /// - public DataHandler(DatabaseSettings xSettings) + public DataHandler(DatabaseSettings xSettings, IFile xFile) { + _xFile = xFile ?? throw new ArgumentNullException(nameof(xFile)); + var sConnectionString = new MySqlConnectionStringBuilder { Server = xSettings.DBhost, @@ -39,7 +42,7 @@ public DataHandler(DatabaseSettings xSettings) /// /// Extracts obituary dictionaries from the stored page JSON structure. /// - public static List> ExtractTrauerData(JsonNode? xData, string sLocalPathRoot) + public List> ExtractTrauerData(JsonNode? xData, string sLocalPathRoot) { var arrResult = new List>(); if (xData is not JsonObject xRoot || xRoot["sections"] is not JsonArray arrSections) @@ -66,11 +69,11 @@ public DataHandler(DatabaseSettings xSettings) } else { - var xFileInfo = new FileInfo(PortedHelpers.GetLocalPath(sLinkHref, sLocalPathRoot)); - var sFullName = xFileInfo.Extension.Length == 0 ? Path.Combine(xFileInfo.DirectoryName ?? string.Empty, xFileInfo.Name + ".json") : xFileInfo.FullName; - if (File.Exists(sFullName)) + var sLocalPath = PortedHelpers.GetLocalPath(sLinkHref, sLocalPathRoot); + var sFullName = Path.HasExtension(sLocalPath) ? sLocalPath : sLocalPath + ".json"; + if (_xFile.Exists(sFullName)) { - xTrauerfallData = JsonNode.Parse(File.ReadAllText(sFullName)) as JsonObject ?? new JsonObject(); + xTrauerfallData = JsonNode.Parse(_xFile.ReadAllText(sFullName)) as JsonObject ?? new JsonObject(); Console.Write(','); } else @@ -98,7 +101,7 @@ public DataHandler(DatabaseSettings xSettings) var sSource = xImage0["src"]?.ToString() ?? string.Empty; if (sSource.Contains("MEDIA", StringComparison.Ordinal)) { - sProfileImagePath = PortedHelpers.GetLocalPath(PortedHelpers.LCropStr(sSource, "?"), sLocalPathRoot); + sProfileImagePath = PortedHelpers.GetLocalPath(sSource.LCropStr("?"), sLocalPathRoot); } } } @@ -166,9 +169,9 @@ public DataHandler(DatabaseSettings xSettings) else { var sPdfFile = dTrauerfall["pdf"]?.ToString() ?? string.Empty; - if (File.Exists(sPdfFile)) + if (_xFile.Exists(sPdfFile)) { - dTrauerfall["pdfText"] = PortedHelpers.PdfText(File.ReadAllBytes(sPdfFile)); + dTrauerfall["pdfText"] = PortedHelpers.PdfText(_xFile.ReadAllBytes(sPdfFile)); } } } @@ -283,20 +286,20 @@ public void BuildTrauerFallIndex() /// public long AppendTrauerFall(Dictionary dTrauerfall) { - var (sLastName, sFirstName) = PortedHelpers.SplitName(PortedHelpers.Cond(dTrauerfall, "name")); + var (sLastName, sFirstName) = dTrauerfall.Cond("name").SplitName(); using var xCommand = new MySqlCommand( "INSERT INTO `Trauerfall` (`URL`, `Created`, `Preread_Birth`, `Preread_Death`, `Fullname`, `Firstname`, `Lastname`, `Birthname`, `Place`, `Created_by`) VALUES (@url, @created, @birth, @death, @fullName, @firstName, @lastName, @birthName, @place, @createdBy);", _dbConn); - xCommand.Parameters.AddWithValue("@url", PortedHelpers.Cond(dTrauerfall, "url")); - xCommand.Parameters.AddWithValue("@created", ToDbValue(PortedHelpers.Str2Date(PortedHelpers.Cond(dTrauerfall, "created_on")))); - xCommand.Parameters.AddWithValue("@birth", ToDbValue(PortedHelpers.Str2Date(TrimLeadingTwo(PortedHelpers.Cond(dTrauerfall, "Birth"))))); - xCommand.Parameters.AddWithValue("@death", ToDbValue(PortedHelpers.Str2Date(TrimLeadingTwo(PortedHelpers.Cond(dTrauerfall, "Death"))))); + xCommand.Parameters.AddWithValue("@url", dTrauerfall.Cond("url")); + xCommand.Parameters.AddWithValue("@created", ToDbValue(PortedHelpers.Str2Date(dTrauerfall.Cond("created_on")))); + xCommand.Parameters.AddWithValue("@birth", ToDbValue(PortedHelpers.Str2Date(TrimLeadingTwo(dTrauerfall.Cond("Birth"))))); + xCommand.Parameters.AddWithValue("@death", ToDbValue(PortedHelpers.Str2Date(TrimLeadingTwo(dTrauerfall.Cond("Death"))))); xCommand.Parameters.AddWithValue("@fullName", $"{sLastName}, {sFirstName}"); xCommand.Parameters.AddWithValue("@firstName", sFirstName); xCommand.Parameters.AddWithValue("@lastName", sLastName); - xCommand.Parameters.AddWithValue("@birthName", PortedHelpers.Cond(dTrauerfall, "Birthname")); - xCommand.Parameters.AddWithValue("@place", PortedHelpers.Cond(dTrauerfall, "Place")); - xCommand.Parameters.AddWithValue("@createdBy", PortedHelpers.Cond(dTrauerfall, "created_by")); + xCommand.Parameters.AddWithValue("@birthName", dTrauerfall.Cond("Birthname")); + xCommand.Parameters.AddWithValue("@place", dTrauerfall.Cond("Place")); + xCommand.Parameters.AddWithValue("@createdBy", dTrauerfall.Cond("created_by")); xCommand.ExecuteNonQuery(); return xCommand.LastInsertedId; } @@ -306,31 +309,31 @@ public long AppendTrauerFall(Dictionary dTrauerfall) /// public long AppendTrauerAnz(long iTrauerfallId, Dictionary dTrauerfall, string sLocalPath) { - var sPath = Directory.GetParent(PortedHelpers.Cond(dTrauerfall, "img"))?.FullName ?? string.Empty; + var sPath = Directory.GetParent(dTrauerfall.Cond("img"))?.FullName ?? string.Empty; var sNormalizedLocalPath = sPath.Replace(sLocalPath, string.Empty, StringComparison.Ordinal); var sProfileBase = Directory.GetParent(Directory.GetParent(sPath)?.FullName ?? string.Empty)?.FullName ?? string.Empty; - var sProfileImage = PortedHelpers.Cond(dTrauerfall, "profImg").Replace(sProfileBase, "..\\..", StringComparison.Ordinal); + var sProfileImage = dTrauerfall.Cond("profImg").Replace(sProfileBase, "..\\..", StringComparison.Ordinal); var iRubrik = GetRubrik(dTrauerfall); - var (sLastName, sFirstName) = PortedHelpers.SplitName(PortedHelpers.Cond(dTrauerfall, "name")); + var (sLastName, sFirstName) = dTrauerfall.Cond("name").SplitName(); using var xCommand = new MySqlCommand( "INSERT INTO `Anzeigen` (`idTrauerfall`, `url`, `Announcement`, `release`,`localpath`, `pngFile`, `pdfFile`, `Additional`, `Firstname`,`Lastname`, `Birthname`, `Birth`, `Death`, `Place`, `Info`, `ProfileImg`, `Rubrik`) VALUES (@idtf, @url, @announcement, @release, @localpath, @pngFile, @pdfFile, @additional, @firstName, @lastName, @birthName, @birth, @death, @place, @info, @profileImg, @rubrik);", _dbConn); xCommand.Parameters.AddWithValue("@idtf", iTrauerfallId); - xCommand.Parameters.AddWithValue("@url", PortedHelpers.Cond(dTrauerfall, "url")); - xCommand.Parameters.AddWithValue("@announcement", int.TryParse(PortedHelpers.Cond(dTrauerfall, "id"), out var iId) ? iId : 0); - xCommand.Parameters.AddWithValue("@release", ToDbValue(PortedHelpers.Str2Date(PortedHelpers.Cond(dTrauerfall, "publish")))); + xCommand.Parameters.AddWithValue("@url", dTrauerfall.Cond("url")); + xCommand.Parameters.AddWithValue("@announcement", int.TryParse(dTrauerfall.Cond("id"), out var iId) ? iId : 0); + xCommand.Parameters.AddWithValue("@release", ToDbValue(PortedHelpers.Str2Date(dTrauerfall.Cond("publish")))); xCommand.Parameters.AddWithValue("@localpath", sNormalizedLocalPath); - xCommand.Parameters.AddWithValue("@pngFile", Path.GetFileName(PortedHelpers.Cond(dTrauerfall, "img"))); - xCommand.Parameters.AddWithValue("@pdfFile", Path.GetFileName(PortedHelpers.Cond(dTrauerfall, "pdf"))); + xCommand.Parameters.AddWithValue("@pngFile", Path.GetFileName(dTrauerfall.Cond("img"))); + xCommand.Parameters.AddWithValue("@pdfFile", Path.GetFileName(dTrauerfall.Cond("pdf"))); xCommand.Parameters.AddWithValue("@additional", JsonSerializer.Serialize(dTrauerfall, PortedHelpers.JsonOptions)); xCommand.Parameters.AddWithValue("@firstName", sFirstName); xCommand.Parameters.AddWithValue("@lastName", sLastName); - xCommand.Parameters.AddWithValue("@birthName", PortedHelpers.Cond(dTrauerfall, "Birthname")); - xCommand.Parameters.AddWithValue("@birth", ToDbValue(PortedHelpers.Str2Date(TrimLeadingTwo(PortedHelpers.Cond(dTrauerfall, "Birth"))))); - xCommand.Parameters.AddWithValue("@death", ToDbValue(PortedHelpers.Str2Date(TrimLeadingTwo(PortedHelpers.Cond(dTrauerfall, "Death"))))); - xCommand.Parameters.AddWithValue("@place", PortedHelpers.Cond(dTrauerfall, "Place")); - xCommand.Parameters.AddWithValue("@info", PortedHelpers.Cond(dTrauerfall, "pdfText")); + xCommand.Parameters.AddWithValue("@birthName", dTrauerfall.Cond("Birthname")); + xCommand.Parameters.AddWithValue("@birth", ToDbValue(PortedHelpers.Str2Date(TrimLeadingTwo(dTrauerfall.Cond("Birth"))))); + xCommand.Parameters.AddWithValue("@death", ToDbValue(PortedHelpers.Str2Date(TrimLeadingTwo(dTrauerfall.Cond("Death"))))); + xCommand.Parameters.AddWithValue("@place", dTrauerfall.Cond("Place")); + xCommand.Parameters.AddWithValue("@info", dTrauerfall.Cond("pdfText")); xCommand.Parameters.AddWithValue("@profileImg", sProfileImage); xCommand.Parameters.AddWithValue("@rubrik", iRubrik); xCommand.ExecuteNonQuery(); @@ -375,7 +378,7 @@ public void SetTrauerAnz(Dictionary dCurrent, Dictionary { ["url"] = PortedHelpers.Cond(dTrauerfall, "url"), diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/PortedHelpers.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/PortedHelpers.cs index 8506800d7..49d2449ba 100644 --- a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/PortedHelpers.cs +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/PortedHelpers.cs @@ -57,17 +57,14 @@ public static class PortedHelpers /// /// Returns a trimmed string value from a loosely typed dictionary. /// - public static string Cond(IReadOnlyDictionary dValues, string sKey) - { - return dValues.TryGetValue(sKey, out var xValue) + public static string Cond(this IReadOnlyDictionary dValues, string sKey) => dValues.TryGetValue(sKey, out var xValue) ? Convert.ToString(xValue, CultureInfo.InvariantCulture)?.Trim(' ') ?? string.Empty : string.Empty; - } /// /// Crops the input string at the first occurrence of the specified separator. /// - public static string LCropStr(string sOriginal, string sSeparator) + public static string LCropStr(this string sOriginal, string sSeparator) { var iFound = sOriginal.IndexOf(sSeparator, StringComparison.Ordinal); return iFound >= 0 ? sOriginal[..iFound] : sOriginal; @@ -76,7 +73,7 @@ public static string LCropStr(string sOriginal, string sSeparator) /// /// Splits a full name into last name and first name using the original Python rules. /// - public static (string LastName, string FirstName) SplitName(string sName) + public static (string LastName, string FirstName) SplitName(this string sName) { var arrNames = sName.Trim(' ').Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray(); if (arrNames.Length == 0) @@ -98,11 +95,11 @@ public static (string LastName, string FirstName) SplitName(string sName) else if (arrNames[iIndex].Contains('-', StringComparison.Ordinal)) { var arrParts = arrNames[iIndex].Split('-', 2); - arrNames[iIndex] = $"{Capitalize(arrParts[0])}-{Capitalize(arrParts.Length > 1 ? arrParts[1] : string.Empty)}"; + arrNames[iIndex] = $"{arrParts[0].Capitalize()}-{(arrParts.Length > 1 ? arrParts[1].Capitalize() : string.Empty)}"; } else { - arrNames[iIndex] = Capitalize(arrNames[iIndex]); + arrNames[iIndex] = arrNames[iIndex].Capitalize(); } } @@ -171,7 +168,7 @@ public static string GetLocalPath(string sUrl, string sLocalPathRoot, DateOnly? else { var iStartIndex = Math.Min(sLocalPath.Length, iFound + 16); - var sDateFragment = LCropStr(sLocalPath.Substring(iStartIndex, Math.Min(10, Math.Max(0, sLocalPath.Length - iStartIndex))), "\\"); + var sDateFragment = sLocalPath.Substring(iStartIndex, Math.Min(10, Math.Max(0, sLocalPath.Length - iStartIndex))).LCropStr("\\"); var arrSplit = sDateFragment.Split('-'); if (arrSplit.Length >= 3) { @@ -187,7 +184,7 @@ public static string GetLocalPath(string sUrl, string sLocalPathRoot, DateOnly? iFound = sLocalPath.IndexOf(sMarker, StringComparison.Ordinal); if (iFound >= 0) { - var sPathPart = LCropStr(sLocalPath[(iFound + sMarker.Length)..], "\\"); + var sPathPart = sLocalPath[(iFound + sMarker.Length)..].LCropStr("\\"); using var xMd5 = MD5.Create(); var arrDigest = xMd5.ComputeHash(Encoding.UTF8.GetBytes(sPathPart)); var iCrc = (((int)arrDigest[^2] << 8) | arrDigest[^1]) & 1023; @@ -223,38 +220,35 @@ public static string PdfText(byte[] arrBytes) /// /// Converts a supported CLR value into a . /// - public static JsonNode? ToJsonNode(object? xValue) + public static JsonNode? ToJsonNode(this object? xValue) => xValue switch { - return xValue switch - { - null => null, - JsonNode xNode => xNode.DeepClone(), - string sValue => JsonValue.Create(sValue), - bool xValue1 => JsonValue.Create(xValue1), - int iValue => JsonValue.Create(iValue), - long iValue => JsonValue.Create(iValue), - double fValue => JsonValue.Create(fValue), - decimal fValue => JsonValue.Create(fValue), - DateOnly dtValue => JsonValue.Create(dtValue.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)), - DateTime dtValue => JsonValue.Create(dtValue.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)), - byte[] arrBytes => JsonValue.Create(Convert.ToBase64String(arrBytes)), - IDictionary dValues => ToJsonObject(new Dictionary(dValues, StringComparer.Ordinal)), - IDictionary dValues => ToJsonObject(dValues.ToDictionary(k => k.Key, v => (object?)v.Value)), - IEnumerable> arrItems => ToJsonArray(arrItems.Cast()), - IEnumerable arrItems when xValue is not string => ToJsonArray(arrItems), - _ => JsonSerializer.SerializeToNode(xValue, JsonOptions) - }; - } + null => null, + JsonNode xNode => xNode.DeepClone(), + string sValue => JsonValue.Create(sValue), + bool xValue1 => JsonValue.Create(xValue1), + int iValue => JsonValue.Create(iValue), + long iValue => JsonValue.Create(iValue), + double fValue => JsonValue.Create(fValue), + decimal fValue => JsonValue.Create(fValue), + DateOnly dtValue => JsonValue.Create(dtValue.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)), + DateTime dtValue => JsonValue.Create(dtValue.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)), + byte[] arrBytes => JsonValue.Create(Convert.ToBase64String(arrBytes)), + IDictionary dValues => ToJsonObject(new Dictionary(dValues, StringComparer.Ordinal)), + IDictionary dValues => ToJsonObject(dValues.ToDictionary(k => k.Key, v => (object?)v.Value)), + IEnumerable> arrItems => ToJsonArray(arrItems.Cast()), + IEnumerable arrItems when xValue is not string => ToJsonArray(arrItems), + _ => JsonSerializer.SerializeToNode(xValue, JsonOptions) + }; /// /// Converts a dictionary into a JSON object. /// - public static JsonObject ToJsonObject(IReadOnlyDictionary dValues) + public static JsonObject ToJsonObject(this IReadOnlyDictionary dValues) { var xObject = new JsonObject(); foreach (var kvValue in dValues) { - xObject[kvValue.Key] = ToJsonNode(kvValue.Value); + xObject[kvValue.Key] = kvValue.Value.ToJsonNode(); } return xObject; @@ -265,13 +259,13 @@ private static JsonArray ToJsonArray(IEnumerable arrItems) var xArray = new JsonArray(); foreach (var xItem in arrItems) { - xArray.Add(ToJsonNode(xItem)); + xArray.Add(xItem.ToJsonNode()); } return xArray; } - private static string Capitalize(string sValue) + public static string Capitalize(this string sValue) { if (string.IsNullOrEmpty(sValue)) { diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/WebHandler.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/WebHandler.cs index 3216541c9..65af00302 100644 --- a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/WebHandler.cs +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/WebHandler.cs @@ -1,7 +1,6 @@ using System.Globalization; using System.Text; using OpenQA.Selenium; -using OpenQA.Selenium.Firefox; namespace RnzTrauer.Core; @@ -33,21 +32,26 @@ public sealed class WebHandler : IDisposable ["a"] = ["title", "target", "href"] }; - private readonly HttpClient _httpClient = new(); private readonly RnzConfig _config; + private readonly IHttpClientProxy _xHttpClient; + private readonly IWebDriverFactory _xWebDriverFactory; + private readonly IProgress? _xProgress; /// /// Initializes a new instance of the class. /// - public WebHandler(RnzConfig xConfig) + public WebHandler(RnzConfig xConfig, IHttpClientProxy xHttpClient, IWebDriverFactory xWebDriverFactory, IProgress? xProgress = null) { _config = xConfig; + _xHttpClient = xHttpClient ?? throw new ArgumentNullException(nameof(xHttpClient)); + _xWebDriverFactory = xWebDriverFactory ?? throw new ArgumentNullException(nameof(xWebDriverFactory)); + _xProgress = xProgress; } /// /// Gets the active Firefox driver instance. /// - public FirefoxDriver? Driver { get; private set; } + public IWebDriver? Driver { get; private set; } /// /// Initializes the RNZ login session. @@ -62,8 +66,7 @@ public void InitPage() { } - var xOptions = new FirefoxOptions(); - Driver = new FirefoxDriver(xOptions); + Driver = _xWebDriverFactory.Create(); Driver.Navigate().GoToUrl(_config.Url); Driver.FindElement(By.Id("emailAddress")).SendKeys(_config.User); Driver.FindElement(By.Id("password")).SendKeys(_config.Password); @@ -143,11 +146,11 @@ public void InitPage() var iCounter = 0; while (!string.IsNullOrEmpty(sNextUrl) && iCounter < iMaxPage) { - Console.WriteLine(xDriver.Url); + _xProgress?.Report(new WebHandlerProgress(xDriver.Url, true)); var (arrSubPages, sUrl, sNext) = WorkMainPage(dPages, arrItems, sAnnouncementType); sNextUrl = sNext; - Console.Write("\nGet Subpages:"); + _xProgress?.Report(new WebHandlerProgress("\nGet Subpages:")); foreach (var sSubPage in arrSubPages) { xDriver.Navigate().GoToUrl(sSubPage + "/anzeigen"); @@ -155,14 +158,14 @@ public void InitPage() WorkSubPage(sUrl, dPages, arrItems); } - Console.WriteLine(); + _xProgress?.Report(new WebHandlerProgress(string.Empty, true)); if (!string.IsNullOrEmpty(sNextUrl)) { xDriver.Navigate().GoToUrl(sNextUrl); while (xDriver.Url == sUrl) { Thread.Sleep(500); - Console.Write('.'); + _xProgress?.Report(new WebHandlerProgress(".")); } } @@ -186,7 +189,7 @@ public void Close() public void Dispose() { Close(); - _httpClient.Dispose(); + _xHttpClient.Dispose(); } private (List SubPages, string Url, string NextUrl) WorkMainPage(Dictionary> dPages, List> arrItems, string sAnnouncementType) @@ -251,13 +254,13 @@ public void Dispose() dCopy[CsSrc] = sDataOriginal; } - Console.Write('.'); + _xProgress?.Report(new WebHandlerProgress(".")); try { var sMediaSource = Convert.ToString(dCopy[CsSrc], CultureInfo.InvariantCulture) ?? string.Empty; if (!string.IsNullOrEmpty(sMediaSource)) { - var xResponse = _httpClient.GetAsync(sMediaSource).GetAwaiter().GetResult(); + var xResponse = _xHttpClient.GetAsync(sMediaSource).GetAwaiter().GetResult(); dCopy[CsData] = xResponse.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult(); dCopy[CsHeader] = xResponse.Headers.Concat(xResponse.Content.Headers) .ToDictionary(h => h.Key, h => string.Join(",", h.Value), StringComparer.OrdinalIgnoreCase); @@ -295,7 +298,7 @@ public void Dispose() } } - Console.WriteLine(sNextUrl); + _xProgress?.Report(new WebHandlerProgress(sNextUrl, true)); } } @@ -364,7 +367,7 @@ private void WorkSubPage(string sUrl, Dictionary dTarget) { try { - var xResponse = _httpClient.GetAsync(sHref).GetAwaiter().GetResult(); + var xResponse = _xHttpClient.GetAsync(sHref).GetAwaiter().GetResult(); dTarget[CsData] = xResponse.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult(); dTarget[CsHeader] = xResponse.Headers.Concat(xResponse.Content.Headers) .ToDictionary(h => h.Key, h => string.Join(",", h.Value), StringComparer.OrdinalIgnoreCase); From 6d2b1b1e0a9a2215122b06425bf5209624b81c71 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 02:39:48 +0200 Subject: [PATCH 30/96] RnzTrauer.Tests --- WinAhnenNew/RnzTrauer/RnzTrauer.Tests/PortedHelpersTests.cs | 4 ++-- WinAhnenNew/RnzTrauer/RnzTrauer.Tests/RnzTrauer.Tests.csproj | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/PortedHelpersTests.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/PortedHelpersTests.cs index ceae44593..1f9fcb9c0 100644 --- a/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/PortedHelpersTests.cs +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/PortedHelpersTests.cs @@ -11,7 +11,7 @@ public sealed class PortedHelpersTests [TestMethod] public void LCropStr_Returns_Left_Part() { - Assert.AreEqual("123", PortedHelpers.LCropStr("1234567", "4")); + Assert.AreEqual("123", "1234567".LCropStr("4")); } [DataTestMethod] @@ -25,7 +25,7 @@ public void LCropStr_Returns_Left_Part() [DataRow(" aa bb-cc ", "Bb-Cc", "Aa")] public void SplitName_Matches_Python_Behaviour(string input, string expectedLastName, string expectedFirstName) { - var (lastName, firstName) = PortedHelpers.SplitName(input); + var (lastName, firstName) = input.SplitName(); Assert.AreEqual(expectedLastName, lastName); Assert.AreEqual(expectedFirstName, firstName); } diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/RnzTrauer.Tests.csproj b/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/RnzTrauer.Tests.csproj index 80a5e8447..b93c939a9 100644 --- a/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/RnzTrauer.Tests.csproj +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/RnzTrauer.Tests.csproj @@ -14,6 +14,7 @@ + From 307d36048b43ac96d725592329579257c9017b81 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 02:39:50 +0200 Subject: [PATCH 31/96] GenFreeWin --- WinAhnenNew/RnzTrauer/Directory.Packages.props | 2 ++ 1 file changed, 2 insertions(+) diff --git a/WinAhnenNew/RnzTrauer/Directory.Packages.props b/WinAhnenNew/RnzTrauer/Directory.Packages.props index 3af7aeb4f..a66e44c5d 100644 --- a/WinAhnenNew/RnzTrauer/Directory.Packages.props +++ b/WinAhnenNew/RnzTrauer/Directory.Packages.props @@ -4,8 +4,10 @@ + + From b091b7abd09926ea2a58febede37e664143b16f3 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 09:17:24 +0200 Subject: [PATCH 32/96] AA16_Usercontrol1 --- .../AA16_Usercontrol1/Directory.Packages.props | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Avalonia_Apps/AA16_UserControl/AA16_Usercontrol1/Directory.Packages.props b/Avalonia_Apps/AA16_UserControl/AA16_Usercontrol1/Directory.Packages.props index f6e12d300..9c8ffde8f 100644 --- a/Avalonia_Apps/AA16_UserControl/AA16_Usercontrol1/Directory.Packages.props +++ b/Avalonia_Apps/AA16_UserControl/AA16_Usercontrol1/Directory.Packages.props @@ -3,10 +3,10 @@ true - - - - + + + + From c1d436c0a43e59ce5f8dd4db576422a047e011ab Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 09:17:32 +0200 Subject: [PATCH 33/96] Avln_AnimationTiming --- .../Avln_AnimationTiming/Avln_AnimationTiming.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Avalonia_Apps/AvlnSamples/Avln_AnimationTiming/Avln_AnimationTiming.csproj b/Avalonia_Apps/AvlnSamples/Avln_AnimationTiming/Avln_AnimationTiming.csproj index 480c544f8..56defa431 100644 --- a/Avalonia_Apps/AvlnSamples/Avln_AnimationTiming/Avln_AnimationTiming.csproj +++ b/Avalonia_Apps/AvlnSamples/Avln_AnimationTiming/Avln_AnimationTiming.csproj @@ -38,10 +38,10 @@ - - - - + + + + From 5d9c293564781fd4eeb1d6132be06b6f8c33d29f Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 09:17:32 +0200 Subject: [PATCH 34/96] Avln_AnimationTimingTests --- .../Avln_AnimationTimingTests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Avalonia_Apps/AvlnSamples/Avln_AnimationTimingTests/Avln_AnimationTimingTests.csproj b/Avalonia_Apps/AvlnSamples/Avln_AnimationTimingTests/Avln_AnimationTimingTests.csproj index cae31cbef..de53f4cc3 100644 --- a/Avalonia_Apps/AvlnSamples/Avln_AnimationTimingTests/Avln_AnimationTimingTests.csproj +++ b/Avalonia_Apps/AvlnSamples/Avln_AnimationTimingTests/Avln_AnimationTimingTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive From 10aed73e934432dc44b94c8b7c91f14299850f78 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 09:17:33 +0200 Subject: [PATCH 35/96] Avln_Brushes --- .../AvlnSamples/Avln_Brushes/Avln_Brushes.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Avalonia_Apps/AvlnSamples/Avln_Brushes/Avln_Brushes.csproj b/Avalonia_Apps/AvlnSamples/Avln_Brushes/Avln_Brushes.csproj index e4ae79327..a7ffd8017 100644 --- a/Avalonia_Apps/AvlnSamples/Avln_Brushes/Avln_Brushes.csproj +++ b/Avalonia_Apps/AvlnSamples/Avln_Brushes/Avln_Brushes.csproj @@ -23,10 +23,10 @@ - - - - + + + + From 1454f3bd1ed715898d4f7aa21f4f1526aa7da7e8 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 09:17:33 +0200 Subject: [PATCH 36/96] Avln_Complex_Layout --- .../Avln_Complex_Layout/Avln_Complex_Layout.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Avalonia_Apps/AvlnSamples/Avln_Complex_Layout/Avln_Complex_Layout.csproj b/Avalonia_Apps/AvlnSamples/Avln_Complex_Layout/Avln_Complex_Layout.csproj index 697a1ecb7..fce6bf589 100644 --- a/Avalonia_Apps/AvlnSamples/Avln_Complex_Layout/Avln_Complex_Layout.csproj +++ b/Avalonia_Apps/AvlnSamples/Avln_Complex_Layout/Avln_Complex_Layout.csproj @@ -18,10 +18,10 @@ - - - - + + + + From 786c13227f3576336365123564c178c60bf426a3 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 09:17:33 +0200 Subject: [PATCH 37/96] Avln_Complex_LayoutTests --- .../Avln_Complex_LayoutTests.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Avalonia_Apps/AvlnSamples/Avln_Complex_LayoutTests/Avln_Complex_LayoutTests.csproj b/Avalonia_Apps/AvlnSamples/Avln_Complex_LayoutTests/Avln_Complex_LayoutTests.csproj index b2280df7e..3a8825780 100644 --- a/Avalonia_Apps/AvlnSamples/Avln_Complex_LayoutTests/Avln_Complex_LayoutTests.csproj +++ b/Avalonia_Apps/AvlnSamples/Avln_Complex_LayoutTests/Avln_Complex_LayoutTests.csproj @@ -4,10 +4,10 @@ false - - + + - + From 08921fe0c75ced01ee0bdf72f32d8b1a00e6f4f7 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 09:17:33 +0200 Subject: [PATCH 38/96] Avln_CustomAnimation --- .../Avln_CustomAnimation/Avln_CustomAnimation.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Avalonia_Apps/AvlnSamples/Avln_CustomAnimation/Avln_CustomAnimation.csproj b/Avalonia_Apps/AvlnSamples/Avln_CustomAnimation/Avln_CustomAnimation.csproj index 13e1fdab5..a2f932f93 100644 --- a/Avalonia_Apps/AvlnSamples/Avln_CustomAnimation/Avln_CustomAnimation.csproj +++ b/Avalonia_Apps/AvlnSamples/Avln_CustomAnimation/Avln_CustomAnimation.csproj @@ -12,10 +12,10 @@ - - - - + + + + From bd82b0c233828b29f1cc6fd276362303d8b093aa Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 09:17:34 +0200 Subject: [PATCH 39/96] Avln_Geometry --- .../AvlnSamples/Avln_Geometry/Avln_Geometry.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Avalonia_Apps/AvlnSamples/Avln_Geometry/Avln_Geometry.csproj b/Avalonia_Apps/AvlnSamples/Avln_Geometry/Avln_Geometry.csproj index 756b41da2..6d49f15f6 100644 --- a/Avalonia_Apps/AvlnSamples/Avln_Geometry/Avln_Geometry.csproj +++ b/Avalonia_Apps/AvlnSamples/Avln_Geometry/Avln_Geometry.csproj @@ -25,10 +25,10 @@ - - - - + + + + From 6b6d2ec5c4baa8b670451fd20a0f56139591402d Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 09:17:34 +0200 Subject: [PATCH 40/96] Avln_Hello_World --- .../AvlnSamples/Avln_Hello_World/Avln_Hello_World.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Avalonia_Apps/AvlnSamples/Avln_Hello_World/Avln_Hello_World.csproj b/Avalonia_Apps/AvlnSamples/Avln_Hello_World/Avln_Hello_World.csproj index 2ad76b5b2..52fbf985a 100644 --- a/Avalonia_Apps/AvlnSamples/Avln_Hello_World/Avln_Hello_World.csproj +++ b/Avalonia_Apps/AvlnSamples/Avln_Hello_World/Avln_Hello_World.csproj @@ -18,10 +18,10 @@ - - - - + + + + From 4e03e5bf2e09c85416d2dcc41bd134b06721f2d3 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 09:17:34 +0200 Subject: [PATCH 41/96] Avln_Hello_WorldTests --- .../Avln_Hello_WorldTests/Avln_Hello_WorldTests.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Avalonia_Apps/AvlnSamples/Avln_Hello_WorldTests/Avln_Hello_WorldTests.csproj b/Avalonia_Apps/AvlnSamples/Avln_Hello_WorldTests/Avln_Hello_WorldTests.csproj index bc4f7f52f..491092efc 100644 --- a/Avalonia_Apps/AvlnSamples/Avln_Hello_WorldTests/Avln_Hello_WorldTests.csproj +++ b/Avalonia_Apps/AvlnSamples/Avln_Hello_WorldTests/Avln_Hello_WorldTests.csproj @@ -9,13 +9,13 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + From 05c5c6099d6c99274ad8c36b53dc7bc4a7dd8d9c Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 09:17:34 +0200 Subject: [PATCH 42/96] Avln_ImageView --- .../AvlnSamples/Avln_ImageView/Avln_ImageView.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Avalonia_Apps/AvlnSamples/Avln_ImageView/Avln_ImageView.csproj b/Avalonia_Apps/AvlnSamples/Avln_ImageView/Avln_ImageView.csproj index e9a8a55ba..2109d3772 100644 --- a/Avalonia_Apps/AvlnSamples/Avln_ImageView/Avln_ImageView.csproj +++ b/Avalonia_Apps/AvlnSamples/Avln_ImageView/Avln_ImageView.csproj @@ -13,10 +13,10 @@ - - - - + + + + From 3c8c91694e71c95986f11786d1c063640be0048f Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 09:17:35 +0200 Subject: [PATCH 43/96] Avln_IntegrationTestApp --- .../Avln_IntegrationTestApp.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Avalonia_Apps/AvlnSamples/Avln_IntegrationTestApp/Avln_IntegrationTestApp.csproj b/Avalonia_Apps/AvlnSamples/Avln_IntegrationTestApp/Avln_IntegrationTestApp.csproj index 78f2bcd89..6674f48b5 100644 --- a/Avalonia_Apps/AvlnSamples/Avln_IntegrationTestApp/Avln_IntegrationTestApp.csproj +++ b/Avalonia_Apps/AvlnSamples/Avln_IntegrationTestApp/Avln_IntegrationTestApp.csproj @@ -22,10 +22,10 @@ - - - - + + + + From d79d60b9d12665f3415938d0284860f0475717b0 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 09:17:35 +0200 Subject: [PATCH 44/96] Avln_MoveWindow --- .../AvlnSamples/Avln_MoveWindow/Avln_MoveWindow.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Avalonia_Apps/AvlnSamples/Avln_MoveWindow/Avln_MoveWindow.csproj b/Avalonia_Apps/AvlnSamples/Avln_MoveWindow/Avln_MoveWindow.csproj index 70a31d004..a4649d15b 100644 --- a/Avalonia_Apps/AvlnSamples/Avln_MoveWindow/Avln_MoveWindow.csproj +++ b/Avalonia_Apps/AvlnSamples/Avln_MoveWindow/Avln_MoveWindow.csproj @@ -18,10 +18,10 @@ - - - - + + + + From 2d17f634fedc1680598cc61e3f1fb08bf2f89af4 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 09:17:35 +0200 Subject: [PATCH 45/96] Avln_MoveWindowTests --- .../Avln_MoveWindowTests/Avln_MoveWindowTests.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Avalonia_Apps/AvlnSamples/Avln_MoveWindowTests/Avln_MoveWindowTests.csproj b/Avalonia_Apps/AvlnSamples/Avln_MoveWindowTests/Avln_MoveWindowTests.csproj index a4c6c6de7..67befede5 100644 --- a/Avalonia_Apps/AvlnSamples/Avln_MoveWindowTests/Avln_MoveWindowTests.csproj +++ b/Avalonia_Apps/AvlnSamples/Avln_MoveWindowTests/Avln_MoveWindowTests.csproj @@ -9,13 +9,13 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + From efa553d3bf0826b4a8af670dfa23981edb0d599b Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 09:17:35 +0200 Subject: [PATCH 46/96] Avln_RenderDemo --- .../AvlnSamples/Avln_RenderDemo/Avln_RenderDemo.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Avalonia_Apps/AvlnSamples/Avln_RenderDemo/Avln_RenderDemo.csproj b/Avalonia_Apps/AvlnSamples/Avln_RenderDemo/Avln_RenderDemo.csproj index d0ab64785..52da7ab4b 100644 --- a/Avalonia_Apps/AvlnSamples/Avln_RenderDemo/Avln_RenderDemo.csproj +++ b/Avalonia_Apps/AvlnSamples/Avln_RenderDemo/Avln_RenderDemo.csproj @@ -13,10 +13,10 @@ - - - - + + + + From f560c89314815950bcb55184e1aaf259760773cc Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 09:17:36 +0200 Subject: [PATCH 47/96] Avln_Sample_Template --- .../Avln_Sample_Template/Avln_Sample_Template.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Avalonia_Apps/AvlnSamples/Avln_Sample_Template/Avln_Sample_Template.csproj b/Avalonia_Apps/AvlnSamples/Avln_Sample_Template/Avln_Sample_Template.csproj index 07111d778..a4b9d0733 100644 --- a/Avalonia_Apps/AvlnSamples/Avln_Sample_Template/Avln_Sample_Template.csproj +++ b/Avalonia_Apps/AvlnSamples/Avln_Sample_Template/Avln_Sample_Template.csproj @@ -18,10 +18,10 @@ - - - - + + + + From 589bbbc3e30ea739b4f8a82aa20b7244a9a6f171 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 09:17:36 +0200 Subject: [PATCH 48/96] Avln_TextTestApp --- .../AvlnSamples/Avln_TextTestApp/Avln_TextTestApp.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Avalonia_Apps/AvlnSamples/Avln_TextTestApp/Avln_TextTestApp.csproj b/Avalonia_Apps/AvlnSamples/Avln_TextTestApp/Avln_TextTestApp.csproj index 57cd58276..aae944e16 100644 --- a/Avalonia_Apps/AvlnSamples/Avln_TextTestApp/Avln_TextTestApp.csproj +++ b/Avalonia_Apps/AvlnSamples/Avln_TextTestApp/Avln_TextTestApp.csproj @@ -10,10 +10,10 @@ - - - - + + + + From bde7150734a8b9d1e30873e25d50879e334279ac Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 09:17:36 +0200 Subject: [PATCH 49/96] SampleControls --- .../AvlnSamples/SampleControls/ControlSamples.Pack.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Avalonia_Apps/AvlnSamples/SampleControls/ControlSamples.Pack.csproj b/Avalonia_Apps/AvlnSamples/SampleControls/ControlSamples.Pack.csproj index b5db976f6..bbc992389 100644 --- a/Avalonia_Apps/AvlnSamples/SampleControls/ControlSamples.Pack.csproj +++ b/Avalonia_Apps/AvlnSamples/SampleControls/ControlSamples.Pack.csproj @@ -23,7 +23,7 @@ - + From a990e8e6dbbb7995cc914b4f10d117f537f066b7 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 09:17:50 +0200 Subject: [PATCH 50/96] Avln_BaseLib --- Avalonia_Apps/Libraries/Avln_BaseLib/Avln_BaseLib.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Avalonia_Apps/Libraries/Avln_BaseLib/Avln_BaseLib.csproj b/Avalonia_Apps/Libraries/Avln_BaseLib/Avln_BaseLib.csproj index 543962b82..de0b78814 100644 --- a/Avalonia_Apps/Libraries/Avln_BaseLib/Avln_BaseLib.csproj +++ b/Avalonia_Apps/Libraries/Avln_BaseLib/Avln_BaseLib.csproj @@ -26,8 +26,8 @@ - - + + From 3cbea4a9934d2a3d0c7feae40a95ea1f83ecee6d Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 09:17:50 +0200 Subject: [PATCH 51/96] Avln_BaseLibTests --- .../Libraries/Avln_BaseLibTests/Avln_BaseLibTests.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Avalonia_Apps/Libraries/Avln_BaseLibTests/Avln_BaseLibTests.csproj b/Avalonia_Apps/Libraries/Avln_BaseLibTests/Avln_BaseLibTests.csproj index 72b06fdb1..99502597f 100644 --- a/Avalonia_Apps/Libraries/Avln_BaseLibTests/Avln_BaseLibTests.csproj +++ b/Avalonia_Apps/Libraries/Avln_BaseLibTests/Avln_BaseLibTests.csproj @@ -16,14 +16,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + From affe7ae99af0b01dc3938b2ac64063bf67618c4f Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 09:17:51 +0200 Subject: [PATCH 52/96] BaseLib --- Avalonia_Apps/Libraries/BaseLib/Helper/ObjectHelper.cs | 1 - Avalonia_Apps/Libraries/BaseLib/Helper/StringUtils.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/Avalonia_Apps/Libraries/BaseLib/Helper/ObjectHelper.cs b/Avalonia_Apps/Libraries/BaseLib/Helper/ObjectHelper.cs index 686e531de..a5d4405ce 100644 --- a/Avalonia_Apps/Libraries/BaseLib/Helper/ObjectHelper.cs +++ b/Avalonia_Apps/Libraries/BaseLib/Helper/ObjectHelper.cs @@ -17,7 +17,6 @@ using System.ComponentModel; using System.Globalization; using System.Linq; -using System.Runtime.CompilerServices; namespace BaseLib.Helper; diff --git a/Avalonia_Apps/Libraries/BaseLib/Helper/StringUtils.cs b/Avalonia_Apps/Libraries/BaseLib/Helper/StringUtils.cs index ccc56783a..4022fe7f8 100644 --- a/Avalonia_Apps/Libraries/BaseLib/Helper/StringUtils.cs +++ b/Avalonia_Apps/Libraries/BaseLib/Helper/StringUtils.cs @@ -18,7 +18,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.InteropServices; namespace BaseLib.Helper; From b5955bdab2bc13ccb0959447d869ce481667b32a Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 09:17:51 +0200 Subject: [PATCH 53/96] BaseLibTests --- Avalonia_Apps/Libraries/BaseLibTests/BaseLibTests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Avalonia_Apps/Libraries/BaseLibTests/BaseLibTests.csproj b/Avalonia_Apps/Libraries/BaseLibTests/BaseLibTests.csproj index 54f402aab..8d729d6ba 100644 --- a/Avalonia_Apps/Libraries/BaseLibTests/BaseLibTests.csproj +++ b/Avalonia_Apps/Libraries/BaseLibTests/BaseLibTests.csproj @@ -15,7 +15,7 @@ $(TargetFrameworks);net10.0 - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 505e7e14879b868b8f28aec927d763f7dded1be2 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 09:17:53 +0200 Subject: [PATCH 54/96] RenderImage.BaseTests --- .../RenderImage.BaseTests/RenderImage.BaseTests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Avalonia_Apps/RenderImage.BaseTests/RenderImage.BaseTests.csproj b/Avalonia_Apps/RenderImage.BaseTests/RenderImage.BaseTests.csproj index d01ad3802..53de2cc54 100644 --- a/Avalonia_Apps/RenderImage.BaseTests/RenderImage.BaseTests.csproj +++ b/Avalonia_Apps/RenderImage.BaseTests/RenderImage.BaseTests.csproj @@ -8,8 +8,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive From 1ff83c11487a5eb31239a6805b7e7d7e7d73ef55 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 09:17:54 +0200 Subject: [PATCH 55/96] Avalonia_Apps --- .../AA21_Buttons/Directory.Packages.props | 8 ++++---- Avalonia_Apps/Packages.props | 20 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Avalonia_Apps/AA21_Buttons/Directory.Packages.props b/Avalonia_Apps/AA21_Buttons/Directory.Packages.props index 0e3eb77d2..acc4fd728 100644 --- a/Avalonia_Apps/AA21_Buttons/Directory.Packages.props +++ b/Avalonia_Apps/AA21_Buttons/Directory.Packages.props @@ -6,15 +6,15 @@ - + - - + + - + diff --git a/Avalonia_Apps/Packages.props b/Avalonia_Apps/Packages.props index 11f10dc58..23d8e688f 100644 --- a/Avalonia_Apps/Packages.props +++ b/Avalonia_Apps/Packages.props @@ -6,20 +6,20 @@ - - - - + + + + - + - + - + - + @@ -31,9 +31,9 @@ - + - + \ No newline at end of file From 550439b1a88a729682ec2adeafed49d043f58e01 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 10:03:35 +0200 Subject: [PATCH 56/96] Add IFile interface and FileProxy for file abstraction Introduced the IFile interface to abstract file system access, enabling testable and interchangeable file operations. Implemented the FileProxy class, which delegates all IFile methods to System.IO.File. This change allows for easier mocking and unit testing of file interactions. --- .../Libraries/BaseLib/Models/FileProxy.cs | 39 +++++++ .../BaseLib/Models/Interfaces/IFile.cs | 103 ++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 Avalonia_Apps/Libraries/BaseLib/Models/FileProxy.cs create mode 100644 Avalonia_Apps/Libraries/BaseLib/Models/Interfaces/IFile.cs diff --git a/Avalonia_Apps/Libraries/BaseLib/Models/FileProxy.cs b/Avalonia_Apps/Libraries/BaseLib/Models/FileProxy.cs new file mode 100644 index 000000000..09c1ab499 --- /dev/null +++ b/Avalonia_Apps/Libraries/BaseLib/Models/FileProxy.cs @@ -0,0 +1,39 @@ +using BaseLib.Models.Interfaces; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BaseLib.Models; + +public class FileProxy : IFile +{ + public bool Exists(string sPath) + => File.Exists(sPath); + public Stream OpenRead(string sPath) + => File.OpenRead(sPath); + public Stream OpenWrite(string sPath) + => File.OpenWrite(sPath); + public Stream Create(string sPath) + => File.Create(sPath); + public string ReadAllText(string sPath) + => File.ReadAllText(sPath); + public string ReadAllText(string sPath, Encoding encoding) + => File.ReadAllText(sPath, encoding); + public void WriteAllText(string sPath, string sContents) + => File.WriteAllText(sPath, sContents); + public void WriteAllText(string sPath, string sContents, Encoding encoding) + => File.WriteAllText(sPath, sContents, encoding); + public byte[] ReadAllBytes(string sPath) + => File.ReadAllBytes(sPath); + public void WriteAllBytes(string sPath, byte[] rgBytes) + => File.WriteAllBytes(sPath, rgBytes); + public void Delete(string sPath) + => File.Delete(sPath); + public void Copy(string sSourceFileName, string sDestFileName, bool xOverwrite) + => File.Copy(sSourceFileName, sDestFileName, xOverwrite); + public void Move(string sSourceFileName, string sDestFileName) + => File.Move(sSourceFileName, sDestFileName); +} diff --git a/Avalonia_Apps/Libraries/BaseLib/Models/Interfaces/IFile.cs b/Avalonia_Apps/Libraries/BaseLib/Models/Interfaces/IFile.cs new file mode 100644 index 000000000..eadf02be8 --- /dev/null +++ b/Avalonia_Apps/Libraries/BaseLib/Models/Interfaces/IFile.cs @@ -0,0 +1,103 @@ +using System.IO; +using System.Text; + +namespace BaseLib.Models.Interfaces; + +/// +/// Provides an abstraction over for testable file system access. +/// +public interface IFile +{ + /// + /// Determines whether the specified file exists. + /// + /// The file path. + /// if the file exists; otherwise . + bool Exists(string sPath); + + /// + /// Opens a file for reading. + /// + /// The file path. + /// A readable stream. + Stream OpenRead(string sPath); + + /// + /// Opens an existing file for writing. + /// + /// The file path. + /// A writable stream. + Stream OpenWrite(string sPath); + + /// + /// Creates or overwrites a file. + /// + /// The file path. + /// A writable stream. + Stream Create(string sPath); + + /// + /// Reads all text from a file using UTF-8 encoding. + /// + /// The file path. + /// The file content. + string ReadAllText(string sPath); + + /// + /// Reads all text from a file using the specified encoding. + /// + /// The file path. + /// The text encoding. + /// The file content. + string ReadAllText(string sPath, Encoding encoding); + + /// + /// Writes text to a file using UTF-8 encoding. + /// + /// The file path. + /// The text content. + void WriteAllText(string sPath, string sContents); + + /// + /// Writes text to a file using the specified encoding. + /// + /// The file path. + /// The text content. + /// The text encoding. + void WriteAllText(string sPath, string sContents, Encoding encoding); + + /// + /// Reads all bytes from a file. + /// + /// The file path. + /// The file content as bytes. + byte[] ReadAllBytes(string sPath); + + /// + /// Writes all bytes to a file. + /// + /// The file path. + /// The byte content. + void WriteAllBytes(string sPath, byte[] rgBytes); + + /// + /// Deletes the specified file. + /// + /// The file path. + void Delete(string sPath); + + /// + /// Copies a file to a new location. + /// + /// The source file path. + /// The destination file path. + /// to overwrite an existing destination file. + void Copy(string sSourceFileName, string sDestFileName, bool xOverwrite); + + /// + /// Moves a file to a new location. + /// + /// The source file path. + /// The destination file path. + void Move(string sSourceFileName, string sDestFileName); +} From c3405b9f64a9832bb9299e94452cb961b5cf45af Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 10:28:30 +0200 Subject: [PATCH 57/96] Remove DataAnnotation disabling, update placeholders, add security Removed DisableAvaloniaDataAnnotationValidation and related usings from multiple App.axaml.cs files. Replaced Watermark with PlaceholderText in TextBox controls across several views for better consistency. Added Tmds.DBus.Protocol 0.92.0 as a security fix (GHSA-xrw6-gwf8-vvr9) in package management files and via Directory.Build.targets. Removed MVVM_25_RichTextEdit_net project reference from the solution. Added WPF_Samples.props to define build and output paths for WPF sample projects. --- .../AA05_CommandParCalc/App.axaml.cs | 19 ------------------- .../AA06_ValueConverter2/App.axaml.cs | 17 ----------------- .../AA09_DialogBoxes/Views/DialogView.axaml | 4 ++-- .../AA09_DialogBoxes/Views/DialogWindow.axaml | 4 ++-- .../AA09_DialogBoxes/Views/MainWindow.axaml | 4 ++-- .../AA15_Labyrinth/AA15a_Treppen/App.axaml.cs | 16 ---------------- .../Directory.Packages.props | 4 ++++ .../AA19_FilterLists/Views/PersonView.axaml | 6 +++--- .../AA21_Buttons/Directory.Packages.props | 4 ++++ .../AA22_AvlnCap/AA22_AvlnCap/App.axaml.cs | 16 ---------------- .../AA22_AvlnCap/AA22_AvlnCap2/App.axaml.cs | 16 ---------------- .../Avln_RichTextEdit/App.axaml.cs | 16 ---------------- .../Avalonia_App_01/App.axaml.cs | 16 ---------------- .../Avalonia_App02/App.axaml.cs | 17 ----------------- Avalonia_Apps/Avalonia_Apps.sln | 3 +-- .../Styles_and_Templates/WPF_Samples.props | 8 ++++++++ Avalonia_Apps/Directory.Build.targets | 7 +++++++ Avalonia_Apps/Packages.props | 4 ++++ 18 files changed, 37 insertions(+), 144 deletions(-) create mode 100644 Avalonia_Apps/AvlnSamples/Styles_and_Templates/WPF_Samples.props create mode 100644 Avalonia_Apps/Directory.Build.targets diff --git a/Avalonia_Apps/AA05_CommandParCalc/AA05_CommandParCalc/App.axaml.cs b/Avalonia_Apps/AA05_CommandParCalc/AA05_CommandParCalc/App.axaml.cs index fbae35880..9933e996c 100644 --- a/Avalonia_Apps/AA05_CommandParCalc/AA05_CommandParCalc/App.axaml.cs +++ b/Avalonia_Apps/AA05_CommandParCalc/AA05_CommandParCalc/App.axaml.cs @@ -1,9 +1,6 @@ using System; -using System.Linq; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Data.Core; -using Avalonia.Data.Core.Plugins; using Avalonia.Markup.Xaml; using Avalonia.Platform; using AA05_CommandParCalc.Models; @@ -44,27 +41,11 @@ protected void InitDesktopApp(IClassicDesktopStyleApplicationLifetime desktop) Services = services.BuildServiceProvider(); - // Avoid duplicate validations from both Avalonia and the CommunityToolkit. - // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins - DisableAvaloniaDataAnnotationValidation(); desktop.MainWindow = new MainWindow { DataContext = Services.GetRequiredService() }; } - private void DisableAvaloniaDataAnnotationValidation() - { - // Get an array of plugins to remove - var dataValidationPluginsToRemove = - BindingPlugins.DataValidators.OfType().ToArray(); - - // remove each entry found - foreach (var plugin in dataValidationPluginsToRemove) - { - BindingPlugins.DataValidators.Remove(plugin); - } - } - public IServiceProvider? Services { get; private set; } } diff --git a/Avalonia_Apps/AA06_ValueConverter2/AA06_ValueConverter2/App.axaml.cs b/Avalonia_Apps/AA06_ValueConverter2/AA06_ValueConverter2/App.axaml.cs index 37d1854b6..3038b3ad9 100644 --- a/Avalonia_Apps/AA06_ValueConverter2/AA06_ValueConverter2/App.axaml.cs +++ b/Avalonia_Apps/AA06_ValueConverter2/AA06_ValueConverter2/App.axaml.cs @@ -1,9 +1,6 @@ using System; -using System.Linq; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Data.Core; -using Avalonia.Data.Core.Plugins; using Avalonia.Markup.Xaml; using Avalonia.Platform; using AA06_ValueConverter2.Models; @@ -46,25 +43,11 @@ protected void InitDesktopApp(IClassicDesktopStyleApplicationLifetime desktop) // Avoid duplicate validations from both Avalonia and the CommunityToolkit. // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins - DisableAvaloniaDataAnnotationValidation(); desktop.MainWindow = new MainWindow { DataContext = Services.GetRequiredService() }; } - private void DisableAvaloniaDataAnnotationValidation() - { - // Get an array of plugins to remove - var dataValidationPluginsToRemove = - BindingPlugins.DataValidators.OfType().ToArray(); - - // remove each entry found - foreach (var plugin in dataValidationPluginsToRemove) - { - BindingPlugins.DataValidators.Remove(plugin); - } - } - public IServiceProvider? Services { get; private set; } } diff --git a/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Views/DialogView.axaml b/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Views/DialogView.axaml index d8c3eafa5..241008945 100644 --- a/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Views/DialogView.axaml +++ b/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Views/DialogView.axaml @@ -10,8 +10,8 @@ - - + + @@ -61,7 +62,8 @@ - + + diff --git a/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEdit/Views/RichTextEditView.axaml.cs b/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEdit/Views/RichTextEditView.axaml.cs index e70c8e5ba..4c65394fc 100644 --- a/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEdit/Views/RichTextEditView.axaml.cs +++ b/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEdit/Views/RichTextEditView.axaml.cs @@ -1,5 +1,7 @@ using Avalonia.Controls; +using Avalonia.VisualTree; using System; +using System.Threading.Tasks; using AA25_RichTextEdit.ViewModels; using Avln_CommonDialogs.Base.Interfaces; @@ -17,29 +19,37 @@ private void RichTextEditView_AttachedToVisualTree(object? sender, Avalonia.Visu { if (DataContext is RichTextEditViewModel vm) { - vm.FileOpenDialog = DoFileDialog; - vm.FileSaveAsDialog = DoFileDialog; + vm.FileOpenDialog = DoOpenFileDialog; + vm.FileSaveAsDialog = DoSaveFileDialog; vm.dPrintDialog = DoPrintDialog; vm.CloseApp = DoClose; } } - private bool? DoFileDialog(string filename, IFileDialog par, Action? onAccept) + private async Task DoOpenFileDialog(string filename, IOpenFileDialog par, Action? onAccept) { - par.FileName = filename; - var window = this.GetVisualRoot() as Window; - bool? result = par.ShowDialog(window); - if (result ?? false) onAccept?.Invoke(par.FileName, par); + var files = await par.ShowAsync(); + bool result = files.Count > 0; + if (result) onAccept?.Invoke(files[0], par); return result; } - private bool? DoPrintDialog(IPrintDialog par, Action? onPrint) + private async Task DoSaveFileDialog(string filename, ISaveFileDialog par, Action? onAccept) { - bool? result = par.ShowAsync().GetAwaiter().GetResult(); - - if (result ?? false) onPrint?.Invoke(par, null); // Avalonia placeholder + par.InitialFileName = filename; + var file = await par.ShowAsync(); + bool result = file != null; + if (result) onAccept?.Invoke(file!, par); return result; } - private void DoClose() => (this)?.Close(); + private async Task DoPrintDialog(IPrintDialog par, Action? onPrint) + { + var session = await par.ShowAsync(); + bool result = session != null; + if (result) onPrint?.Invoke(par, session); + return result; + } + + private void DoClose() => (TopLevel.GetTopLevel(this) as Window)?.Close(); } diff --git a/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/AA25_RichTextEditTests.csproj b/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/AA25_RichTextEditTests.csproj index a6d9fb6f8..5236f5ed4 100644 --- a/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/AA25_RichTextEditTests.csproj +++ b/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/AA25_RichTextEditTests.csproj @@ -4,6 +4,7 @@ net8.0;net9.0 true false + C:\Projekte\GitHub\Avalonia_Apps\bin\AA25_RichTextEditTests\Debug\net8.0\fine-code-coverage\coverage-tool-output\AA25_RichTextEditTests %28net8.0%29-fcc-mscodecoverage-generated.runsettings diff --git a/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/AppTests.cs b/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/AppTests.cs index 67791c063..f314e5c0d 100644 --- a/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/AppTests.cs +++ b/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/AppTests.cs @@ -11,7 +11,7 @@ internal class TestApp : App { public void DoStartUp() { - OnStartup(null); + OnFrameworkInitializationCompleted(); } } [TestClass()] diff --git a/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/Models/RichTextEditModelTests.cs b/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/Models/RichTextEditModelTests.cs index c141261f1..a86598bf7 100644 --- a/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/Models/RichTextEditModelTests.cs +++ b/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/Models/RichTextEditModelTests.cs @@ -13,7 +13,7 @@ // *********************************************************************** using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.VisualStudio.TestTools.UnitTesting; -using MVVM.ViewModel; +using Avalonia.ViewModels; using NSubstitute; using System.ComponentModel; using BaseLib.Models.Interfaces; diff --git a/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/Models/SimpleLogTests.cs b/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/Models/SimpleLogTests.cs index 299fea26c..823996c3f 100644 --- a/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/Models/SimpleLogTests.cs +++ b/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/Models/SimpleLogTests.cs @@ -1,6 +1,6 @@ using BaseLib.Models.Interfaces; using Microsoft.VisualStudio.TestTools.UnitTesting; -using MVVM.ViewModel; +using Avalonia.ViewModels; using NSubstitute; using System; using System.Linq; @@ -10,7 +10,7 @@ namespace AA25_RichTextEdit.Models.Tests; [TestClass] public class SimpleLogTests:BaseTestViewModel { - private Action _gsOld; + private Action? _gsOld; private ISysTime? _sysTime; private SimpleLog simpleLog; @@ -29,10 +29,10 @@ public void TestInitialize() [TestCleanup] public void TestCleanup() { - SimpleLog.LogAction = _gsOld; + SimpleLog.LogAction = _gsOld!; } - [DataTestMethod] + [TestMethod] [DataRow("Test message",new[] { "08/24/2022 12:00:00: Msg: Test message\r\n" })] [DataRow(null, new[] { "08/24/2022 12:00:00: Msg: \r\n" })] [DataRow("Some other test", new[] { "08/24/2022 12:00:00: Msg: Some other test\r\n" })] diff --git a/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/ViewModels/MainWindowViewModelTests.cs b/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/ViewModels/MainWindowViewModelTests.cs index 1bb45a252..e9e58702a 100644 --- a/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/ViewModels/MainWindowViewModelTests.cs +++ b/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/ViewModels/MainWindowViewModelTests.cs @@ -13,7 +13,7 @@ // *********************************************************************** using Microsoft.VisualStudio.TestTools.UnitTesting; using System.ComponentModel; -using MVVM.ViewModel; +using Avalonia.ViewModels; /// /// The Tests namespace. diff --git a/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/ViewModels/RichTextEditViewModelTests.cs b/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/ViewModels/RichTextEditViewModelTests.cs index ccc50a3cd..64581f517 100644 --- a/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/ViewModels/RichTextEditViewModelTests.cs +++ b/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/ViewModels/RichTextEditViewModelTests.cs @@ -15,7 +15,7 @@ using System.ComponentModel; using Microsoft.VisualStudio.TestTools.UnitTesting; using NSubstitute; -using MVVM.ViewModel; +using Avalonia.ViewModels; using BaseLib.Helper; using AA25_RichTextEdit.Models; From 9792d3e95e37f6088fac35eb284a43ff921f8cb3 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 11:24:58 +0200 Subject: [PATCH 59/96] AA06_Converters4Tests --- .../ValueConverter/Bool2VisibilityConverterTests.cs | 4 ++-- .../View/Converter/WindowPortToGridLinesTests.cs | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Avalonia_Apps/AA06_ValueConverter2/AA06_Converters4Tests/ValueConverter/Bool2VisibilityConverterTests.cs b/Avalonia_Apps/AA06_ValueConverter2/AA06_Converters4Tests/ValueConverter/Bool2VisibilityConverterTests.cs index 48bd6f42f..7df82247d 100644 --- a/Avalonia_Apps/AA06_ValueConverter2/AA06_Converters4Tests/ValueConverter/Bool2VisibilityConverterTests.cs +++ b/Avalonia_Apps/AA06_ValueConverter2/AA06_Converters4Tests/ValueConverter/Bool2VisibilityConverterTests.cs @@ -40,7 +40,7 @@ public class Bool2VisibilityConverterTests /// The value. /// The expected. /// - [DataTestMethod] + [TestMethod] [DataRow(10.5, true)] [DataRow(0.99, true)] [DataRow(true, true)] @@ -58,7 +58,7 @@ public void ConvertTest(object? value, bool expected) /// Defines the test method ConvertBackTest. /// /// - [DataTestMethod()] + [TestMethod()] [DataRow(true, true)] [DataRow(false, false)] [DataRow(false, null)] diff --git a/Avalonia_Apps/AA06_ValueConverter2/AA06_Converters4Tests/View/Converter/WindowPortToGridLinesTests.cs b/Avalonia_Apps/AA06_ValueConverter2/AA06_Converters4Tests/View/Converter/WindowPortToGridLinesTests.cs index 48ebbc218..91100eddc 100644 --- a/Avalonia_Apps/AA06_ValueConverter2/AA06_Converters4Tests/View/Converter/WindowPortToGridLinesTests.cs +++ b/Avalonia_Apps/AA06_ValueConverter2/AA06_Converters4Tests/View/Converter/WindowPortToGridLinesTests.cs @@ -8,7 +8,9 @@ namespace AA06_Converters_4.View.Converter.Tests; [TestClass()] public class WindowPortToGridLinesTests { +#pragma warning disable CS8618 // Ein Non-Nullable-Feld muss beim Beenden des Konstruktors einen Wert ungleich NULL enthalten. Fügen Sie ggf. den „erforderlichen“ Modifizierer hinzu, oder deklarieren Sie den Modifizierer als NULL-Werte zulassend. WindowPortToGridLines testVC; +#pragma warning restore CS8618 // Ein Non-Nullable-Feld muss beim Beenden des Konstruktors einen Wert ungleich NULL enthalten. Fügen Sie ggf. den „erforderlichen“ Modifizierer hinzu, oder deklarieren Sie den Modifizierer als NULL-Werte zulassend. ViewModels.SWindowPort wp; public static IEnumerable ConvertTestData @@ -34,7 +36,7 @@ public void WindowPortToGridLinesTest() Assert.Fail(); } - [DataTestMethod()] + [TestMethod()] [DynamicData(nameof(ConvertTestData))] public void ConvertTest(object o) { From 329cebd8067c0dafaf9ca651549f41f910d19a5b Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 11:25:03 +0200 Subject: [PATCH 60/96] AA15_LabyrinthTests --- .../AA15_LabyrinthTests/AA15_LabyrinthTests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Avalonia_Apps/AA15_Labyrinth/AA15_LabyrinthTests/AA15_LabyrinthTests.csproj b/Avalonia_Apps/AA15_Labyrinth/AA15_LabyrinthTests/AA15_LabyrinthTests.csproj index 47341c74d..54d404bb2 100644 --- a/Avalonia_Apps/AA15_Labyrinth/AA15_LabyrinthTests/AA15_LabyrinthTests.csproj +++ b/Avalonia_Apps/AA15_Labyrinth/AA15_LabyrinthTests/AA15_LabyrinthTests.csproj @@ -1,4 +1,4 @@ - + net8.0;net9.0 From f5d5e4e6d08a41ecdb18022b7b22bb40131cc40c Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 11:25:13 +0200 Subject: [PATCH 61/96] AA25_RichTextEditTests --- .../AA25_RichTextEditTests/AA25_RichTextEditTests.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/AA25_RichTextEditTests.csproj b/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/AA25_RichTextEditTests.csproj index 5236f5ed4..a6d9fb6f8 100644 --- a/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/AA25_RichTextEditTests.csproj +++ b/Avalonia_Apps/AA25_RichTextEdit/AA25_RichTextEditTests/AA25_RichTextEditTests.csproj @@ -4,7 +4,6 @@ net8.0;net9.0 true false - C:\Projekte\GitHub\Avalonia_Apps\bin\AA25_RichTextEditTests\Debug\net8.0\fine-code-coverage\coverage-tool-output\AA25_RichTextEditTests %28net8.0%29-fcc-mscodecoverage-generated.runsettings From ad054ce66598218b71bdb6850a87b92773dfd957 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sat, 11 Apr 2026 11:25:16 +0200 Subject: [PATCH 62/96] Avalonia_App_01.Browser --- .../Avalonia_App_01.Browser.csproj | 2 +- .../Avalonia_App_01.Browser/Program.cs | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Avalonia_Apps/Avalonia_App01/Avalonia_App_01.Browser/Avalonia_App_01.Browser.csproj b/Avalonia_Apps/Avalonia_App01/Avalonia_App_01.Browser/Avalonia_App_01.Browser.csproj index 0e121a5ce..e91fa7bb8 100644 --- a/Avalonia_Apps/Avalonia_App01/Avalonia_App_01.Browser/Avalonia_App_01.Browser.csproj +++ b/Avalonia_Apps/Avalonia_App01/Avalonia_App_01.Browser/Avalonia_App_01.Browser.csproj @@ -1,7 +1,7 @@  - net8.0-browser + net10.0-browser Exe true enable diff --git a/Avalonia_Apps/Avalonia_App01/Avalonia_App_01.Browser/Program.cs b/Avalonia_Apps/Avalonia_App01/Avalonia_App_01.Browser/Program.cs index b1b50b9ea..9409927de 100644 --- a/Avalonia_Apps/Avalonia_App01/Avalonia_App_01.Browser/Program.cs +++ b/Avalonia_Apps/Avalonia_App01/Avalonia_App_01.Browser/Program.cs @@ -4,11 +4,18 @@ using Avalonia.Browser; using Avalonia_App_01; +[assembly: SupportedOSPlatform("browser")] + internal sealed partial class Program { - private static Task Main(string[] args) => BuildAvaloniaApp() - .WithInterFont() - .StartBrowserAppAsync("out"); + private static async Task Main(string[] args) + { + await BrowserAppBuilder.StartBrowserAppAsync( + BuildAvaloniaApp() + .WithInterFont(), + "out", + new BrowserPlatformOptions()); + } public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure(); From 3fa884bb1a6d468753c52960d93e8da8562acd5b Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 09:55:58 +0200 Subject: [PATCH 63/96] Add overlay message box with async request support Added OverlayMessageRequestMessage for async Yes/No dialogs. Created MessageBoxWindow (XAML + Code-Behind) to display messages with title, content, and Yes/No buttons. Keyboard shortcuts (J/Enter for Yes, N/Escape for No) and button click events close the dialog with the appropriate result. Focus is set to the Yes button when the window opens. --- .../Messages/OverlayMessageRequestMessage.cs | 15 +++++++ .../Views/MessageBoxWindow.axaml | 1 + .../Views/MessageBoxWindow.axaml.cs | 44 +++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Messages/OverlayMessageRequestMessage.cs create mode 100644 Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Views/MessageBoxWindow.axaml create mode 100644 Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Views/MessageBoxWindow.axaml.cs diff --git a/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Messages/OverlayMessageRequestMessage.cs b/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Messages/OverlayMessageRequestMessage.cs new file mode 100644 index 000000000..fcb313af4 --- /dev/null +++ b/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Messages/OverlayMessageRequestMessage.cs @@ -0,0 +1,15 @@ +using CommunityToolkit.Mvvm.Messaging.Messages; + +namespace AA09_DialogBoxes.Messages; + +public sealed class OverlayMessageRequestMessage : AsyncRequestMessage +{ + public string Title { get; } + public string Content { get; } + + public OverlayMessageRequestMessage(string title, string content) + { + Title = title; + Content = content; + } +} diff --git a/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Views/MessageBoxWindow.axaml b/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Views/MessageBoxWindow.axaml new file mode 100644 index 000000000..37546d496 --- /dev/null +++ b/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Views/MessageBoxWindow.axaml @@ -0,0 +1 @@ + diff --git a/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Views/MessageBoxWindow.axaml.cs b/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Views/MessageBoxWindow.axaml.cs new file mode 100644 index 000000000..84c010754 --- /dev/null +++ b/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Views/MessageBoxWindow.axaml.cs @@ -0,0 +1,44 @@ +using Avalonia.Controls; +using Avalonia.Input; +using AA09_DialogBoxes.Messages; + +namespace AA09_DialogBoxes.Views; + +public partial class MessageBoxWindow : Window +{ + public MessageBoxWindow(string title, string content) + { + InitializeComponent(); + Title = title; + MessageText.Text = content; + this.Opened += (_, _) => YesButton.Focus(); + this.KeyDown += MessageBoxWindow_KeyDown; + } + + private void MessageBoxWindow_KeyDown(object? sender, KeyEventArgs e) + { + switch (e.Key) + { + case Key.J: + case Key.Enter: + Close(MsgBoxResult.Yes); + e.Handled = true; + break; + case Key.N: + case Key.Escape: + Close(MsgBoxResult.No); + e.Handled = true; + break; + } + } + + private void OnYesClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + Close(MsgBoxResult.Yes); + } + + private void OnNoClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + Close(MsgBoxResult.No); + } +} From e83d5b647d7da954c774f59a8c061d1400596eb1 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 10:08:43 +0200 Subject: [PATCH 64/96] Add draggable overlay dialog component for Avalonia New OverlayDialogControl (XAML + C#) provides a movable, theme-aware overlay dialog with clear close button, scrollable content, and fixed action buttons ("Yes"/"No"). Keyboard shortcuts (J/Enter/ N/Escape) are supported. Updated copilot-instructions.md with dialog UX guidelines regarding drag hints, close button visibility, and button placement. --- Avalonia_Apps/.github/copilot-instructions.md | 5 + .../Views/OverlayDialogControl.axaml | 75 ++++++++++ .../Views/OverlayDialogControl.axaml.cs | 131 ++++++++++++++++++ 3 files changed, 211 insertions(+) create mode 100644 Avalonia_Apps/.github/copilot-instructions.md create mode 100644 Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Views/OverlayDialogControl.axaml create mode 100644 Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Views/OverlayDialogControl.axaml.cs diff --git a/Avalonia_Apps/.github/copilot-instructions.md b/Avalonia_Apps/.github/copilot-instructions.md new file mode 100644 index 000000000..259de3338 --- /dev/null +++ b/Avalonia_Apps/.github/copilot-instructions.md @@ -0,0 +1,5 @@ +# Copilot Instructions + +## Projektrichtlinien +- For dialog UX, keep extra drag hint text optional: omit it for native-like message boxes but keep clear visual titlebar affordance for in-window overlay dialogs; ensure action buttons remain fixed/always visible with long content. +- Ensure the overlay close button is clearly visible at the outer edge with strong hover feedback, and keep important action buttons fully within dialog bounds (no overflow). \ No newline at end of file diff --git a/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Views/OverlayDialogControl.axaml b/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Views/OverlayDialogControl.axaml new file mode 100644 index 000000000..751857e7f --- /dev/null +++ b/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Views/OverlayDialogControl.axaml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Views/OverlayDialogControl.axaml.cs b/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Views/OverlayDialogControl.axaml.cs new file mode 100644 index 000000000..8bf61e898 --- /dev/null +++ b/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Views/OverlayDialogControl.axaml.cs @@ -0,0 +1,131 @@ +using System; +using System.Threading.Tasks; +using AA09_DialogBoxes.Messages; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Styling; + +namespace AA09_DialogBoxes.Views; + +public partial class OverlayDialogControl : UserControl +{ + private TaskCompletionSource? _overlayCompletion; + private bool _isDraggingOverlay; + private Point _dragOffset; + + public OverlayDialogControl() + { + InitializeComponent(); + KeyDown += OverlayDialogControl_KeyDown; + } + + public Task ShowAsync(Window owner, string title, string content) + { + if (_overlayCompletion is { Task.IsCompleted: false }) + return _overlayCompletion.Task; + + _overlayCompletion = new(TaskCreationOptions.RunContinuationsAsynchronously); + + OverlayTitle.Text = title; + OverlayContent.Text = content; + UpdateBackdrop(owner.ActualThemeVariant); + IsVisible = true; + CenterOverlayDialog(owner.Bounds); + Focus(); + + return _overlayCompletion.Task; + } + + private void OverlayDialogControl_KeyDown(object? sender, KeyEventArgs e) + { + if (!IsVisible || _overlayCompletion is null) + return; + + switch (e.Key) + { + case Key.J: + case Key.Enter: + Finish(MsgBoxResult.Yes); + e.Handled = true; + break; + case Key.N: + case Key.Escape: + Finish(MsgBoxResult.No); + e.Handled = true; + break; + } + } + + private void UpdateBackdrop(ThemeVariant theme) + { + OverlayBackdrop.Background = theme == ThemeVariant.Dark + ? new SolidColorBrush(Color.Parse("#88000000")) + : new SolidColorBrush(Color.Parse("#88FFFFFF")); + } + + private void CenterOverlayDialog(Rect ownerBounds) + { + var dialogWidth = OverlayDialog.Bounds.Width > 0 ? OverlayDialog.Bounds.Width : OverlayDialog.Width; + var dialogHeight = OverlayDialog.Bounds.Height > 0 ? OverlayDialog.Bounds.Height : OverlayDialog.Height; + + var left = Math.Max(0, (ownerBounds.Width - dialogWidth) / 2d); + var top = Math.Max(0, (ownerBounds.Height - dialogHeight) / 2d); + Canvas.SetLeft(OverlayDialog, left); + Canvas.SetTop(OverlayDialog, top); + } + + private void Finish(MsgBoxResult result) + { + IsVisible = false; + _overlayCompletion?.TrySetResult(result); + _overlayCompletion = null; + } + + private void OverlayYes_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + => Finish(MsgBoxResult.Yes); + + private void OverlayNo_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + => Finish(MsgBoxResult.No); + + private void OverlayClose_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + => Finish(MsgBoxResult.No); + + private void OverlayDialog_PointerPressed(object? sender, PointerPressedEventArgs e) + { + if (sender is not Control control) + return; + + _isDraggingOverlay = true; + var pointerPos = e.GetPosition(OverlayCanvas); + var left = Canvas.GetLeft(OverlayDialog); + var top = Canvas.GetTop(OverlayDialog); + if (double.IsNaN(left)) left = 0; + if (double.IsNaN(top)) top = 0; + _dragOffset = new Point(pointerPos.X - left, pointerPos.Y - top); + e.Pointer.Capture(control); + } + + private void OverlayDialog_PointerMoved(object? sender, PointerEventArgs e) + { + if (!_isDraggingOverlay) + return; + + var pointerPos = e.GetPosition(OverlayCanvas); + var newLeft = pointerPos.X - _dragOffset.X; + var newTop = pointerPos.Y - _dragOffset.Y; + + var maxLeft = Math.Max(0, OverlayCanvas.Bounds.Width - OverlayDialog.Bounds.Width); + var maxTop = Math.Max(0, OverlayCanvas.Bounds.Height - OverlayDialog.Bounds.Height); + + Canvas.SetLeft(OverlayDialog, Math.Clamp(newLeft, 0, maxLeft)); + Canvas.SetTop(OverlayDialog, Math.Clamp(newTop, 0, maxTop)); + } + + private void OverlayDialog_PointerReleased(object? sender, PointerReleasedEventArgs e) + { + _isDraggingOverlay = false; + e.Pointer.Capture(null); + } +} From e7d50a26241368e5d7d185b5723bf94d13d6dbd9 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:03:54 +0200 Subject: [PATCH 65/96] TraceCsv2realCsv --- CSharpBible/AboutExTests/AboutExTests.csproj | 4 +- .../Basic_Del00_TemplateTests.csproj | 4 +- .../Basic_Del01_ActionTests.csproj | 4 +- .../Basic_Del02_FilterTests.csproj | 4 +- .../Basic_Del03_GeneralTests.csproj | 4 +- ...Basic_Del04_TestImposibleStuffTests.csproj | 4 +- .../TestConsoleTests/TestConsoleTests.csproj | 2 +- CSharpBible/DB/FoxCon/FoxCon.csproj | 2 +- .../DataAnalysis.Core.Tests.csproj | 2 +- .../RepoMigrator.App.State.csproj | 2 +- .../ViewModels/MainViewModel.cs | 25 +- .../RepoMigrator.Core/IMigrationService.cs | 2 + .../Services/MigrationService.cs | 40 +- .../RepoMigrator.Tests.csproj | 4 +- .../Filters/IInputFilter.cs | 32 ++ .../Filters/IOutputFilter.cs | 19 + .../Models/Interfaces/ITraceDataSet.cs | 23 + .../Models/Interfaces/ITraceFieldMetadata.cs | 25 + .../Models/Interfaces/ITraceMetadata.cs | 16 + .../Models/Interfaces/ITraceRecord.cs | 24 + .../TraceAnalysis.Base/Models/TraceDataSet.cs | 39 ++ .../Models/TraceFieldMetadata.cs | 60 +++ .../Models/TraceMetadata.cs | 28 ++ .../TraceAnalysis.Base/Models/TraceRecord.cs | 28 ++ .../TraceAnalysis.Base.csproj | 16 + .../Filters/CsvOutputFilter.cs | 62 +++ .../Filters/FlatCsvInputFilter.cs | 100 ++++ .../Filters/TraceCsvInputFilter.cs | 98 ++++ .../Model/CsvModel.cs | 467 +++++++++--------- .../Model/TraceCSVReader.cs | 132 ++--- .../TraceAnalysis.Filter.CSV.csproj | 22 + CSharpBible/Data/TraceCsv2realCsv/Program.cs | 2 +- .../TraceCsv2realCsv/TraceCsv2realCsv.csproj | 6 +- .../Model/CsvModelTests.cs | 1 + .../TraceCsv2realCsvTests.csproj | 8 +- .../CustomerRepositoryTests.csproj | 2 +- .../AsteroidsModernEngine.Tests.csproj | 4 +- .../DetectiveGame.Tests.csproj | 4 +- .../Galaxia_BaseTests.csproj | 4 +- .../Galaxia_UI.Tests/Galaxia_UI.Tests.csproj | 2 +- .../Game_BaseTests/Game_BaseTests.csproj | 4 +- .../RemoteTerminal.Tests.csproj | 2 +- .../Snake_BaseTests/Snake_BaseTests.csproj | 4 +- .../Sokoban_BaseTests.csproj | 4 +- .../Sudoku_BaseTests/Sudoku_BaseTests.csproj | 4 +- .../Tetris_BaseTests/Tetris_BaseTests.csproj | 4 +- .../TileSetAnimator.Tests.csproj | 2 +- .../Treppen.BaseTests.csproj | 4 +- .../VTileEdit.WPFTests.csproj | 4 +- .../VTileEditTests/VTileEditTests.csproj | 4 +- .../Werner_Flaschbier_BaseTests.csproj | 4 +- .../Werner_Flaschbier_ConsoleTests.csproj | 4 +- .../MVVM_ImageHandlingTests.csproj | 4 +- .../MVVM_ImageHandling_netTests.csproj | 4 +- .../MarbleBoard.Engine.Tests.csproj | 4 +- .../PermutationTests/PermutationTests.csproj | 4 +- .../ScreenX.BaseTests.csproj | 2 +- .../Libraries/BaseLib/Models/FileProxy.cs | 39 ++ .../BaseLib/Models/Interfaces/IFile.cs | 103 ++++ .../BaseLibTests/BaseLibTests.csproj | 2 +- .../ConsoleDisplayTests.csproj | 2 +- .../ConsoleLibTests/ConsoleLibTests.csproj | 4 +- .../MathLibraryTests/MathLibraryTests.csproj | 2 +- .../DemoLibraryTests/DemoLibraryTests.csproj | 4 +- .../ItemsControlTut3_netTests.csproj | 4 +- .../ItemsControlTut4_netTests.csproj | 4 +- .../ListBindingTests/ListBindingTests.csproj | 4 +- .../ListBinding_netTests.csproj | 4 +- .../MVVM_00_IoCTemplateTests.csproj | 4 +- .../MVVM_00_IoCTemplate_netTests.csproj | 4 +- .../MVVM_00_TemplateTests.csproj | 4 +- .../MVVM_00_Template_netTests.csproj | 4 +- .../MVVM_00a_CTTemplateTests.csproj | 4 +- .../MVVM_00a_CTTemplate_netTests.csproj | 4 +- .../MVVM_03_NotifyChangeTests.csproj | 4 +- .../MVVM_03_NotifyChange_netTests.csproj | 4 +- .../MVVM_03a_CTNotifyChangeTests.csproj | 4 +- .../MVVM_03a_CTNotifyChange_netTests.csproj | 4 +- .../MVVM_04_DelegateCommandTests.csproj | 4 +- .../MVVM_04_DelegateCommand_netTests.csproj | 4 +- .../MVVM_04a_CTRelayCommandTests.csproj | 4 +- .../MVVM_04a_CTRelayCommand_netTests.csproj | 4 +- .../MVVM_05_CommandParCalculatorTests.csproj | 4 +- ...VM_05_CommandParCalculator_netTests.csproj | 4 +- .../MVVM_05a_CTCommandParCalcTests.csproj | 4 +- .../MVVM_05a_CTCommandParCalc_netTests.csproj | 4 +- .../MVVM_06_ConvertersTests.csproj | 4 +- .../MVVM_06_Converters_3Tests.csproj | 4 +- .../MVVM_06_Converters_3_netTests.csproj | 4 +- .../MVVM_06_Converters_4Tests.csproj | 4 +- .../MVVM_06_Converters_4_netTests.csproj | 4 +- .../MVVM_09_DialogBoxesTest.csproj | 4 +- .../MVVM_09_DialogBoxes_netTests.csproj | 4 +- .../MVVM_09a_CTDialogBoxesTests.csproj | 4 +- .../MVVM_09a_CTDialogBoxes_netTests.csproj | 4 +- .../MVVM_16_UserControl1Tests.csproj | 4 +- .../MVVM_16_UserControl1_netTests.csproj | 4 +- .../MVVM_17_1_CSV_LadenTests.csproj | 4 +- .../MVVM_17_1_CSV_Laden_netTests.csproj | 4 +- .../MVVM_18_MultiConvertersTests.csproj | 4 +- .../MVVM_19_FilterListsTests.csproj | 4 +- .../MVVM_19_FilterLists_netTests.csproj | 4 +- .../MVVM_20_SysdialogsTests.csproj | 4 +- .../MVVM_20_Sysdialogs_netTests.csproj | 4 +- .../MVVM_20a_CTSysdialogsTests.csproj | 4 +- .../MVVM_20a_CTSysdialogs_netTests.csproj | 4 +- .../MVVM_22_CTWpfCapTests.csproj | 4 +- .../MVVM_22_CTWpfCap_netTests.csproj | 4 +- .../MVVM_22_WpfCapTests.csproj | 4 +- .../MVVM_22_WpfCap_netTests.csproj | 4 +- .../MVVM_24_UserControlTests.csproj | 4 +- .../MVVM_24_UserControl_netTests.csproj | 4 +- .../MVVM_24a_CTUserControlTests.csproj | 4 +- .../MVVM_24a_CTUserControl_netTests.csproj | 4 +- .../MVVM_24b_UserControlTests.csproj | 4 +- .../MVVM_24b_UserControl_netTests.csproj | 4 +- .../MVVM_24c_CTUserControlTests.csproj | 4 +- .../MVVM_24c_CTUserControl_netTests.csproj | 4 +- .../MVVM_25_RichTextEditTests.csproj | 4 +- .../MVVM_25_RichTextEdit_netTests.csproj | 4 +- .../MVVM_27_DataGridTests.csproj | 4 +- .../MVVM_27_DataGrid_netTests.csproj | 4 +- .../MVVM_28_1_CTDataGridExtTests.csproj | 4 +- .../MVVM_28_1_CTDataGridExt_netTests.csproj | 4 +- .../MVVM_28_1_DataGridExtTests.csproj | 4 +- .../MVVM_28_1_DataGridExt_netTests.csproj | 4 +- .../MVVM_28_DataGridTests.csproj | 4 +- .../MVVM_28_DataGrid_netTests.csproj | 4 +- .../MVVM_31_Validation1Tests.csproj | 4 +- .../MVVM_31_Validation1_netTests.csproj | 4 +- .../MVVM_31_Validation2Tests.csproj | 4 +- .../MVVM_31_Validation2_netTests.csproj | 4 +- .../MVVM_31a_CTValidation1Tests.csproj | 4 +- .../MVVM_31a_CTValidation1_netTests.csproj | 4 +- .../MVVM_31a_CTValidation2Tests.csproj | 4 +- .../MVVM_31a_CTValidation2_netTests.csproj | 4 +- .../MVVM_31a_CTValidation3Tests.csproj | 4 +- .../MVVM_31a_CTValidation3_netTests.csproj | 4 +- .../MVVM_33_Events_to_CommandsTests.csproj | 4 +- ...MVVM_33_Events_to_Commands_netTests.csproj | 4 +- .../MVVM_33a_CTEvents_To_CommandsTests.csproj | 4 +- ...M_33a_CTEvents_To_Commands_netTests.csproj | 4 +- .../MVVM_34_BindingEventArgsTests.csproj | 4 +- .../MVVM_34_BindingEventArgs_netTests.csproj | 4 +- .../MVVM_34a_CTBindingEventArgsTests.csproj | 4 +- ...VVM_34a_CTBindingEventArgs_netTests.csproj | 4 +- .../MVVM_35_CommunityToolkitTests.csproj | 4 +- .../MVVM_35_CommunityToolkit_netTests.csproj | 4 +- .../MVVM_36_ComToolKtSavesWorkTests.csproj | 4 +- ...MVVM_36_ComToolKtSavesWork_netTests.csproj | 4 +- .../MVVM_37_TreeViewTests.csproj | 4 +- .../MVVM_37_TreeView_netTests.csproj | 4 +- .../MVVM_38_CTDependencyInjectionTests.csproj | 4 +- ...M_38_CTDependencyInjection_netTests.csproj | 4 +- .../MVVM_39_MultiModelTestTests.csproj | 4 +- .../MVVM_39_MultiModelTest_netTests.csproj | 4 +- .../MVVM_40_WizzardTests.csproj | 4 +- .../MVVM_40_Wizzard_netTests.csproj | 4 +- .../MVVM_41_SudokuTests.csproj | 4 +- .../MVVM_41_Sudoku_netTests.csproj | 4 +- .../MVVM_99_SomeIssueTests.csproj | 4 +- .../MVVM_99_SomeIssue_netTests.csproj | 4 +- .../MVVM_AllExamplesTests.csproj | 4 +- .../MVVM_AllExamples_netTests.csproj | 4 +- .../WpfAppTests/WpfAppTests.csproj | 4 +- .../WpfAppTests/WpfApp_netTests.csproj | 4 +- .../Pattern_00_TemplateTests.csproj | 4 +- .../Pattern_01_SingletonTests.csproj | 4 +- .../Pattern_02_ObserverTests.csproj | 4 +- .../SomeThing/QuineTest/QuineTest.csproj | 2 +- .../SomeThing2Tests/SomeThing2Tests.csproj | 4 +- .../SomeThing2aTests/SomeThing2aTests.csproj | 4 +- CSharpBible/Tests/Test.csproj | 4 +- .../WPF_AnimationTimingTests.csproj | 4 +- .../WPF_AnimationTiming_netTests.csproj | 4 +- .../WPF_Complex_LayoutTests.csproj | 4 +- .../WPF_Complex_Layout_netTests.csproj | 4 +- .../WPF_ControlsAndLayoutTests.csproj | 4 +- .../WPF_ControlsAndLayout_netTests.csproj | 4 +- .../WPF_CustomAnimationTests.csproj | 4 +- .../WPF_CustomAnimation_netTests.csproj | 4 +- .../WPF_Hello_WorldTests.csproj | 4 +- .../WPF_Hello_World_netTests.csproj | 4 +- .../WPF_MasterDetailTests.csproj | 4 +- .../WPF_MasterDetail_netTests.csproj | 4 +- .../WPF_MoveWindowTests.csproj | 4 +- .../WPF_MoveWindow_netTests.csproj | 4 +- .../WPF_Sample_TemplateTests.csproj | 4 +- .../WPF_Sample_Template_netTests.csproj | 4 +- .../WPF_StickyNotesDemoTests.csproj | 4 +- .../WPF_StickyNotesDemo_netTests.csproj | 4 +- 191 files changed, 1399 insertions(+), 652 deletions(-) create mode 100644 CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Filters/IInputFilter.cs create mode 100644 CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Filters/IOutputFilter.cs create mode 100644 CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/Interfaces/ITraceDataSet.cs create mode 100644 CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/Interfaces/ITraceFieldMetadata.cs create mode 100644 CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/Interfaces/ITraceMetadata.cs create mode 100644 CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/Interfaces/ITraceRecord.cs create mode 100644 CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/TraceDataSet.cs create mode 100644 CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/TraceFieldMetadata.cs create mode 100644 CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/TraceMetadata.cs create mode 100644 CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/TraceRecord.cs create mode 100644 CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/TraceAnalysis.Base.csproj create mode 100644 CSharpBible/Data/TraceAnalysis/TraceAnalysis.Filter.CSV/Filters/CsvOutputFilter.cs create mode 100644 CSharpBible/Data/TraceAnalysis/TraceAnalysis.Filter.CSV/Filters/FlatCsvInputFilter.cs create mode 100644 CSharpBible/Data/TraceAnalysis/TraceAnalysis.Filter.CSV/Filters/TraceCsvInputFilter.cs rename CSharpBible/Data/{TraceCsv2realCsv => TraceAnalysis/TraceAnalysis.Filter.CSV}/Model/CsvModel.cs (96%) rename CSharpBible/Data/{TraceCsv2realCsv => TraceAnalysis/TraceAnalysis.Filter.CSV}/Model/TraceCSVReader.cs (95%) create mode 100644 CSharpBible/Data/TraceAnalysis/TraceAnalysis.Filter.CSV/TraceAnalysis.Filter.CSV.csproj create mode 100644 CSharpBible/Libraries/BaseLib/Models/FileProxy.cs create mode 100644 CSharpBible/Libraries/BaseLib/Models/Interfaces/IFile.cs diff --git a/CSharpBible/AboutExTests/AboutExTests.csproj b/CSharpBible/AboutExTests/AboutExTests.csproj index 0d43f6f74..18492525f 100644 --- a/CSharpBible/AboutExTests/AboutExTests.csproj +++ b/CSharpBible/AboutExTests/AboutExTests.csproj @@ -12,8 +12,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/Basics/Basic_Del00_TemplateTests/Basic_Del00_TemplateTests.csproj b/CSharpBible/Basics/Basic_Del00_TemplateTests/Basic_Del00_TemplateTests.csproj index e1a8e3500..c5f6bc9db 100644 --- a/CSharpBible/Basics/Basic_Del00_TemplateTests/Basic_Del00_TemplateTests.csproj +++ b/CSharpBible/Basics/Basic_Del00_TemplateTests/Basic_Del00_TemplateTests.csproj @@ -13,8 +13,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/Basics/Basic_Del01_ActionTests/Basic_Del01_ActionTests.csproj b/CSharpBible/Basics/Basic_Del01_ActionTests/Basic_Del01_ActionTests.csproj index f0d0c967d..2dfe0441e 100644 --- a/CSharpBible/Basics/Basic_Del01_ActionTests/Basic_Del01_ActionTests.csproj +++ b/CSharpBible/Basics/Basic_Del01_ActionTests/Basic_Del01_ActionTests.csproj @@ -13,8 +13,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/Basics/Basic_Del02_FilterTests/Basic_Del02_FilterTests.csproj b/CSharpBible/Basics/Basic_Del02_FilterTests/Basic_Del02_FilterTests.csproj index 0baf77e5c..c4c3bd395 100644 --- a/CSharpBible/Basics/Basic_Del02_FilterTests/Basic_Del02_FilterTests.csproj +++ b/CSharpBible/Basics/Basic_Del02_FilterTests/Basic_Del02_FilterTests.csproj @@ -13,8 +13,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/Basics/Basic_Del03_GeneralTests/Basic_Del03_GeneralTests.csproj b/CSharpBible/Basics/Basic_Del03_GeneralTests/Basic_Del03_GeneralTests.csproj index 5a812c3bb..4866b24ff 100644 --- a/CSharpBible/Basics/Basic_Del03_GeneralTests/Basic_Del03_GeneralTests.csproj +++ b/CSharpBible/Basics/Basic_Del03_GeneralTests/Basic_Del03_GeneralTests.csproj @@ -13,8 +13,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/Basics/Basic_Del04_TestImposibleStuffTests/Basic_Del04_TestImposibleStuffTests.csproj b/CSharpBible/Basics/Basic_Del04_TestImposibleStuffTests/Basic_Del04_TestImposibleStuffTests.csproj index a35304b0a..dffc503a3 100644 --- a/CSharpBible/Basics/Basic_Del04_TestImposibleStuffTests/Basic_Del04_TestImposibleStuffTests.csproj +++ b/CSharpBible/Basics/Basic_Del04_TestImposibleStuffTests/Basic_Del04_TestImposibleStuffTests.csproj @@ -13,8 +13,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/ConsoleApps/TestConsoleTests/TestConsoleTests.csproj b/CSharpBible/ConsoleApps/TestConsoleTests/TestConsoleTests.csproj index 8007f278e..8fc972ff9 100644 --- a/CSharpBible/ConsoleApps/TestConsoleTests/TestConsoleTests.csproj +++ b/CSharpBible/ConsoleApps/TestConsoleTests/TestConsoleTests.csproj @@ -19,7 +19,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/DB/FoxCon/FoxCon.csproj b/CSharpBible/DB/FoxCon/FoxCon.csproj index 87686e905..67db10a4b 100644 --- a/CSharpBible/DB/FoxCon/FoxCon.csproj +++ b/CSharpBible/DB/FoxCon/FoxCon.csproj @@ -19,7 +19,7 @@ - + diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core.Tests/DataAnalysis.Core.Tests.csproj b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core.Tests/DataAnalysis.Core.Tests.csproj index d26d35186..caadd8cd1 100644 --- a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core.Tests/DataAnalysis.Core.Tests.csproj +++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core.Tests/DataAnalysis.Core.Tests.csproj @@ -9,7 +9,7 @@ - + diff --git a/CSharpBible/Data/RepoMigrator/RepoMigrator.App.State/RepoMigrator.App.State.csproj b/CSharpBible/Data/RepoMigrator/RepoMigrator.App.State/RepoMigrator.App.State.csproj index 05e9a5095..e32f201da 100644 --- a/CSharpBible/Data/RepoMigrator/RepoMigrator.App.State/RepoMigrator.App.State.csproj +++ b/CSharpBible/Data/RepoMigrator/RepoMigrator.App.State/RepoMigrator.App.State.csproj @@ -12,7 +12,7 @@ - + diff --git a/CSharpBible/Data/RepoMigrator/RepoMigrator.App.Wpf/ViewModels/MainViewModel.cs b/CSharpBible/Data/RepoMigrator/RepoMigrator.App.Wpf/ViewModels/MainViewModel.cs index 308060ffb..4d9d5bdfb 100644 --- a/CSharpBible/Data/RepoMigrator/RepoMigrator.App.Wpf/ViewModels/MainViewModel.cs +++ b/CSharpBible/Data/RepoMigrator/RepoMigrator.App.Wpf/ViewModels/MainViewModel.cs @@ -32,7 +32,6 @@ public partial class MainViewModel : ObservableObject, IMigrationProgress private string? _sSelectedSvnFromRevisionId; private string? _sSelectedSvnToRevisionId; private string? _sCurrentChangeSetId; - private WorkflowStage _workflowStage = WorkflowStage.Setup; private string? _targetUser; private string? _targetPassword; private bool _xUsePipelinedMigration; @@ -214,6 +213,14 @@ public string? SelectedSvnToRevisionId [NotifyPropertyChangedFor(nameof(IsIdle))] [NotifyPropertyChangedFor(nameof(CanStartMigration))] public partial bool IsRunning { get; set; } + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsSetupStage))] + [NotifyPropertyChangedFor(nameof(IsOptionsStage))] + [NotifyPropertyChangedFor(nameof(IsExecutionStage))] + [NotifyPropertyChangedFor(nameof(ShowCompactEndpointSummaries))] + public partial WorkflowStage WorkflowStage { get; private set; } = WorkflowStage.Setup; + public bool IsIdle => !IsRunning; public bool CanConfigureGitHistory => SourceType == RepoType.Git && TargetType == RepoType.Git; public bool CanConfigureSvnRevisions => SourceType == RepoType.Svn; @@ -365,20 +372,6 @@ public void Report(MigrationReportSeverity severity, MigrationReportMessage mess Append(FormatProgressMessage(message, arrAdditional)); } - public WorkflowStage WorkflowStage - { - get => _workflowStage; - private set - { - if (!SetProperty(ref _workflowStage, value)) - return; - - OnPropertyChanged(nameof(IsSetupStage)); - OnPropertyChanged(nameof(IsOptionsStage)); - OnPropertyChanged(nameof(IsExecutionStage)); - OnPropertyChanged(nameof(ShowCompactEndpointSummaries)); - } - } private void ApplyProgressState(MigrationReportMessage message, object?[] arrAdditional) { @@ -854,7 +847,7 @@ private void SetWorkflowStage(WorkflowStage workflowStage) if (_isLoadingInputState) return; - if (IsRunning && workflowStage != WorkflowStage.Execution) + if (IsRunning && _migration.IsRunning && workflowStage != WorkflowStage.Execution && WorkflowStage == WorkflowStage.Execution) return; WorkflowStage = workflowStage; diff --git a/CSharpBible/Data/RepoMigrator/RepoMigrator.Core/IMigrationService.cs b/CSharpBible/Data/RepoMigrator/RepoMigrator.Core/IMigrationService.cs index a3f9ec61d..912c0ab6e 100644 --- a/CSharpBible/Data/RepoMigrator/RepoMigrator.Core/IMigrationService.cs +++ b/CSharpBible/Data/RepoMigrator/RepoMigrator.Core/IMigrationService.cs @@ -5,6 +5,8 @@ namespace RepoMigrator.Core; /// public interface IMigrationService { + bool IsRunning { get; } + /// /// Migrates changes from a source repository endpoint to a target repository endpoint. /// diff --git a/CSharpBible/Data/RepoMigrator/RepoMigrator.Core/Services/MigrationService.cs b/CSharpBible/Data/RepoMigrator/RepoMigrator.Core/Services/MigrationService.cs index 8f13dd35c..5baa97149 100644 --- a/CSharpBible/Data/RepoMigrator/RepoMigrator.Core/Services/MigrationService.cs +++ b/CSharpBible/Data/RepoMigrator/RepoMigrator.Core/Services/MigrationService.cs @@ -7,7 +7,7 @@ namespace RepoMigrator.Core; public sealed class MigrationService : IMigrationService { private readonly IProviderFactory _factory; - + public bool IsRunning { get; private set; } public MigrationService(IProviderFactory factory) => _factory = factory; public async Task MigrateAsync( @@ -18,24 +18,32 @@ public async Task MigrateAsync( IMigrationProgress progress, CancellationToken ct) { - if (options.TransferMode == RepositoryTransferMode.NativeHistory) + try { - await using var src = _factory.Create(source.Type); - progress.Report(MigrationReportSeverity.Information, MigrationReportMessage.SourceOpening, src.Name); - await src.OpenAsync(source, ct); - progress.Report(MigrationReportSeverity.Information, MigrationReportMessage.NativeHistoryTransferStarting, src.Name, target.Type); - await src.TransferAsync(source, target, options, progress, ct); - progress.Report(MigrationReportSeverity.Information, MigrationReportMessage.MigrationCompleted); - return; - } + IsRunning = true; + if (options.TransferMode == RepositoryTransferMode.NativeHistory) + { + await using var src = _factory.Create(source.Type); + progress.Report(MigrationReportSeverity.Information, MigrationReportMessage.SourceOpening, src.Name); + await src.OpenAsync(source, ct); + progress.Report(MigrationReportSeverity.Information, MigrationReportMessage.NativeHistoryTransferStarting, src.Name, target.Type); + await src.TransferAsync(source, target, options, progress, ct); + progress.Report(MigrationReportSeverity.Information, MigrationReportMessage.MigrationCompleted); + return; + } - if (options.ExecutionMode == MigrationExecutionMode.Pipelined && CanUsePipelinedExecution(source, target)) - { - await MigrateSnapshotsPipelinedAsync(source, target, query, options, progress, ct); - return; - } + if (options.ExecutionMode == MigrationExecutionMode.Pipelined && CanUsePipelinedExecution(source, target)) + { + await MigrateSnapshotsPipelinedAsync(source, target, query, options, progress, ct); + return; + } - await MigrateSnapshotsSequentialAsync(source, target, query, options, progress, ct); + await MigrateSnapshotsSequentialAsync(source, target, query, options, progress, ct); + } + finally + { + IsRunning = false; + } } private async Task MigrateSnapshotsSequentialAsync( diff --git a/CSharpBible/Data/RepoMigrator/RepoMigrator.Tests/RepoMigrator.Tests.csproj b/CSharpBible/Data/RepoMigrator/RepoMigrator.Tests/RepoMigrator.Tests.csproj index d5a311c3d..d2d3b8d44 100644 --- a/CSharpBible/Data/RepoMigrator/RepoMigrator.Tests/RepoMigrator.Tests.csproj +++ b/CSharpBible/Data/RepoMigrator/RepoMigrator.Tests/RepoMigrator.Tests.csproj @@ -7,8 +7,8 @@ enable - - + + diff --git a/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Filters/IInputFilter.cs b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Filters/IInputFilter.cs new file mode 100644 index 000000000..971d3b8f3 --- /dev/null +++ b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Filters/IInputFilter.cs @@ -0,0 +1,32 @@ +using System.IO; +using TraceAnalysis.Base.Models.Interfaces; + +namespace TraceAnalysis.Base.Filters +{ + /// + /// Contract for pluggable input filters that convert a source stream + /// into the canonical structure. + /// + public interface IInputFilter + { + /// + /// Determines whether this filter can handle the given stream. + /// Implementations should inspect the stream without permanently consuming its content. + /// + /// The source stream to inspect. + /// An identifier for the source, e.g. a file path or label. + /// true if this filter can process the stream; otherwise false. + bool CanHandle(Stream _stream, string _sourceId); + + /// + /// Reads the source stream and returns the canonical trace data set. + /// Parse errors are collected in rather than thrown. + /// + /// The source stream to read from. + /// An identifier for the source, e.g. a file path or label. + /// + /// A canonical with all imported records and any parse errors. + /// + ITraceDataSet Read(Stream _stream, string _sourceId); + } +} diff --git a/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Filters/IOutputFilter.cs b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Filters/IOutputFilter.cs new file mode 100644 index 000000000..2a2365ba5 --- /dev/null +++ b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Filters/IOutputFilter.cs @@ -0,0 +1,19 @@ +using System.IO; +using TraceAnalysis.Base.Models.Interfaces; + +namespace TraceAnalysis.Base.Filters +{ + /// + /// Contract for pluggable output filters that serialize the canonical + /// structure to a target stream. + /// + public interface IOutputFilter + { + /// + /// Writes the canonical trace data set to the target stream. + /// + /// The canonical data set to export. + /// The target stream to write to. + void Write(ITraceDataSet _dataSet, Stream _stream); + } +} diff --git a/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/Interfaces/ITraceDataSet.cs b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/Interfaces/ITraceDataSet.cs new file mode 100644 index 000000000..a04c68ba1 --- /dev/null +++ b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/Interfaces/ITraceDataSet.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace TraceAnalysis.Base.Models.Interfaces +{ + /// + /// The canonical intermediate structure that carries imported trace data + /// between input and output filters. + /// + public interface ITraceDataSet + { + /// Source-level metadata describing the origin and field layout of the data set. + ITraceMetadata Metadata { get; } + + /// Ordered list of canonical trace records. + IReadOnlyList Records { get; } + + /// + /// Errors collected during parsing. + /// Processing continues with partial results when errors occur. + /// + IReadOnlyList ParseErrors { get; } + } +} diff --git a/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/Interfaces/ITraceFieldMetadata.cs b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/Interfaces/ITraceFieldMetadata.cs new file mode 100644 index 000000000..89308c3c7 --- /dev/null +++ b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/Interfaces/ITraceFieldMetadata.cs @@ -0,0 +1,25 @@ +using System; + +namespace TraceAnalysis.Base.Models.Interfaces +{ + /// + /// Describes the metadata of a single field within a trace data set. + /// + public interface ITraceFieldMetadata + { + /// Field name as it appears in the source. + string sName { get; } + + /// + /// Inferred field group derived from a shared name prefix and separator, + /// e.g. "AGV1" from "AGV1.X", or null if no group is applicable. + /// + string? sGroup { get; } + + /// Optional format string for display or export purposes. + string? sFormat { get; } + + /// CLR type of the field values, or null if unknown. + Type? FieldType { get; } + } +} diff --git a/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/Interfaces/ITraceMetadata.cs b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/Interfaces/ITraceMetadata.cs new file mode 100644 index 000000000..feea6fa81 --- /dev/null +++ b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/Interfaces/ITraceMetadata.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace TraceAnalysis.Base.Models.Interfaces +{ + /// + /// Describes source-level metadata for a complete trace data set. + /// + public interface ITraceMetadata + { + /// Identifies the source of the trace data, e.g. a file path or stream label. + string sSourceId { get; } + + /// Ordered list of field metadata definitions for the data set. + IReadOnlyList Fields { get; } + } +} diff --git a/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/Interfaces/ITraceRecord.cs b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/Interfaces/ITraceRecord.cs new file mode 100644 index 000000000..f2d1551f1 --- /dev/null +++ b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/Interfaces/ITraceRecord.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace TraceAnalysis.Base.Models.Interfaces +{ + /// + /// Represents a single canonical trace row. + /// The timestamp is the only mandatory canonical field; + /// all other field values are optional and are passed through unchanged. + /// + public interface ITraceRecord + { + /// + /// Mandatory canonical timestamp or row key. + /// All other values are optional. + /// + object Timestamp { get; } + + /// + /// Field values keyed by field name; null when a value is absent + /// for that field in this record. + /// + IReadOnlyDictionary Values { get; } + } +} diff --git a/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/TraceDataSet.cs b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/TraceDataSet.cs new file mode 100644 index 000000000..1f55d44b8 --- /dev/null +++ b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/TraceDataSet.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using TraceAnalysis.Base.Models.Interfaces; + +namespace TraceAnalysis.Base.Models +{ + /// + /// Default implementation of . + /// + public class TraceDataSet : ITraceDataSet + { + private readonly List _parseErrors = new(); + + /// + public ITraceMetadata Metadata { get; } + + /// + public IReadOnlyList Records { get; } + + /// + public IReadOnlyList ParseErrors => _parseErrors; + + /// + /// Initializes a new instance of . + /// + /// Source-level metadata for this data set. + /// Ordered list of canonical trace records. + /// Errors collected during parsing, if any. + public TraceDataSet( + ITraceMetadata _metadata, + IReadOnlyList _records, + IEnumerable? _errors = null) + { + Metadata = _metadata; + Records = _records; + if (_errors != null) + _parseErrors.AddRange(_errors); + } + } +} diff --git a/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/TraceFieldMetadata.cs b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/TraceFieldMetadata.cs new file mode 100644 index 000000000..667c8e6f4 --- /dev/null +++ b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/TraceFieldMetadata.cs @@ -0,0 +1,60 @@ +using System; +using TraceAnalysis.Base.Models.Interfaces; + +namespace TraceAnalysis.Base.Models +{ + /// + /// Default implementation of . + /// + public class TraceFieldMetadata : ITraceFieldMetadata + { + /// + public string sName { get; } + + /// + public string? sGroup { get; } + + /// + public string? sFormat { get; } + + /// + public Type? FieldType { get; } + + /// + /// Initializes a new instance of . + /// The field group is inferred from the field name when not provided explicitly. + /// + /// Field name as it appears in the source. + /// CLR type of the field values, or null if unknown. + /// + /// Optional field group; when null the group is inferred from the field name + /// using '.' or '_' as structural separators. + /// + /// Optional format string for display or export. + public TraceFieldMetadata(string _name, Type? _fieldType = null, string? _group = null, string? _format = null) + { + sName = _name; + FieldType = _fieldType; + sGroup = _group ?? InferGroup(_name); + sFormat = _format; + } + + /// + /// Infers a field group from the field name using '.' or '_' as structural + /// separators (e.g. "AGV1" from "AGV1.X"). + /// Returns null if no separator with a non-empty prefix is found. + /// + private static string? InferGroup(string _name) + { + var dotIndex = _name.IndexOf('.'); + if (dotIndex > 0) + return _name.Substring(0, dotIndex); + + var underscoreIndex = _name.IndexOf('_'); + if (underscoreIndex > 0) + return _name.Substring(0, underscoreIndex); + + return null; + } + } +} diff --git a/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/TraceMetadata.cs b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/TraceMetadata.cs new file mode 100644 index 000000000..47545e8bf --- /dev/null +++ b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/TraceMetadata.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using TraceAnalysis.Base.Models.Interfaces; + +namespace TraceAnalysis.Base.Models +{ + /// + /// Default implementation of . + /// + public class TraceMetadata : ITraceMetadata + { + /// + public string sSourceId { get; } + + /// + public IReadOnlyList Fields { get; } + + /// + /// Initializes a new instance of . + /// + /// Identifier for the trace source, e.g. a file path or stream label. + /// Ordered field metadata definitions. + public TraceMetadata(string _sourceId, IReadOnlyList _fields) + { + sSourceId = _sourceId; + Fields = _fields; + } + } +} diff --git a/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/TraceRecord.cs b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/TraceRecord.cs new file mode 100644 index 000000000..66fff6b66 --- /dev/null +++ b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/Models/TraceRecord.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using TraceAnalysis.Base.Models.Interfaces; + +namespace TraceAnalysis.Base.Models +{ + /// + /// Default implementation of . + /// + public class TraceRecord : ITraceRecord + { + /// + public object Timestamp { get; } + + /// + public IReadOnlyDictionary Values { get; } + + /// + /// Initializes a new instance of . + /// + /// Mandatory timestamp or row key for this record. + /// Field values keyed by field name. + public TraceRecord(object _timestamp, IReadOnlyDictionary _values) + { + Timestamp = _timestamp; + Values = _values; + } + } +} diff --git a/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/TraceAnalysis.Base.csproj b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/TraceAnalysis.Base.csproj new file mode 100644 index 000000000..a49b2325e --- /dev/null +++ b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Base/TraceAnalysis.Base.csproj @@ -0,0 +1,16 @@ + + + + Library + net462;net472;net48;net481;net6.0;net7.0;net8.0 + + + + + $(TargetFrameworks);net9.0 + + + $(TargetFrameworks);net10.0 + + + diff --git a/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Filter.CSV/Filters/CsvOutputFilter.cs b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Filter.CSV/Filters/CsvOutputFilter.cs new file mode 100644 index 000000000..8164e2fb4 --- /dev/null +++ b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Filter.CSV/Filters/CsvOutputFilter.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.IO; +using TraceAnalysis.Base.Filters; +using TraceAnalysis.Base.Models.Interfaces; +using TraceAnalysis.Filter.CSV.Model; + +namespace TraceAnalysis.Filter.CSV.Filters +{ + /// + /// Output filter that serializes the canonical + /// to a tab-separated CSV stream. + /// + public class CsvOutputFilter : IOutputFilter + { + private readonly char _separator; + + /// + /// Initializes a new instance of . + /// + /// + /// Column separator character. Defaults to '\t' (tab). + /// + public CsvOutputFilter(char _separator = '\t') + { + this._separator = _separator; + } + + /// + /// + /// The first written column is the timestamp / row key. + /// Subsequent columns follow the field order defined in + /// . + /// Missing optional field values are written as empty cells. + /// + public void Write(ITraceDataSet _dataSet, Stream _stream) + { + var model = new CsvModel(); + + // Build header: timestamp key column first, then one column per field. + var header = new List<(string name, System.Type? type)> + { + ("TimeBase", typeof(object)) + }; + foreach (var field in _dataSet.Metadata.Fields) + header.Add((field.sName, field.FieldType)); + + model.SetHeader(header); + + // Append one row per canonical record. + foreach (var record in _dataSet.Records) + { + var rowValues = new List { record.Timestamp }; + foreach (var field in _dataSet.Metadata.Fields) + rowValues.Add(record.Values.TryGetValue(field.sName, out var v) ? v : null); + + model.AppendData(rowValues); + } + + model.WriteCSV(_stream, _separator); + } + } +} diff --git a/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Filter.CSV/Filters/FlatCsvInputFilter.cs b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Filter.CSV/Filters/FlatCsvInputFilter.cs new file mode 100644 index 000000000..fc58810c2 --- /dev/null +++ b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Filter.CSV/Filters/FlatCsvInputFilter.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using TraceAnalysis.Base.Filters; +using TraceAnalysis.Base.Models; +using TraceAnalysis.Base.Models.Interfaces; +using TraceAnalysis.Filter.CSV.Model; + +namespace TraceAnalysis.Filter.CSV.Filters +{ + /// + /// Input filter for flat CSV files with a header row. + /// Supports common separators (;, \t, ,). + /// + public class FlatCsvInputFilter : IInputFilter + { + /// Expected first line of a TraceCsv stream (used for format exclusion). + private const string TraceCsvHeader = "[key]; [value]"; + + /// + /// + /// Detects the format by file extension (.csv) first, + /// then confirms the stream header is a flat CSV header row + /// (i.e. not a TraceCsv stream). + /// The stream position is restored after inspection. + /// Returns false when the stream does not support seeking. + /// + public bool CanHandle(Stream _stream, string _sourceId) + { + if (!_stream.CanSeek) + return false; + + var ext = System.IO.Path.GetExtension(_sourceId); + if (!string.IsNullOrEmpty(ext) && !ext.Equals(".csv", StringComparison.OrdinalIgnoreCase)) + return false; + + var startPos = _stream.Position; + try + { + using var reader = new StreamReader(_stream, Encoding.UTF8, true, 1024, leaveOpen: true); + var firstLine = reader.ReadLine(); + return !string.IsNullOrEmpty(firstLine) && firstLine != TraceCsvHeader; + } + finally + { + _stream.Position = startPos; + } + } + + /// + /// + /// Reads the flat CSV stream into a and converts it + /// to the canonical structure. + /// The first column is used as the mandatory timestamp / row key. + /// All remaining columns are treated as optional value fields. + /// Parse errors are collected in . + /// + public ITraceDataSet Read(Stream _stream, string _sourceId) + { + var errors = new List(); + var model = new CsvModel(); + + try + { + model.ReadCsv(_stream); + } + catch (Exception ex) + { + errors.Add($"Failed to read flat CSV from '{_sourceId}': {ex.Message}"); + return new TraceDataSet( + new TraceMetadata(_sourceId, Array.Empty()), + Array.Empty(), + errors); + } + + // Build field metadata from header, skipping the first column (= timestamp key). + var fields = new List(); + for (var i = 1; i < model.Header.Count; i++) + fields.Add(new TraceFieldMetadata(model.Header[i].name, model.Header[i].type)); + + // Convert each row to a canonical record. + var records = new List(); + for (var i = 0; i < model.Rows.Count; i++) + { + var row = model.Rows[i]; + var timestampKey = model.Header.Count > 0 ? model.Header[0].name : string.Empty; + var timestamp = row.TryGetValue(timestampKey, out var ts) ? ts : (object)i; + var values = new Dictionary(); + foreach (var kvp in row) + if (kvp.Key != timestampKey) + values[kvp.Key] = kvp.Value; + + records.Add(new TraceRecord(timestamp, values)); + } + + return new TraceDataSet(new TraceMetadata(_sourceId, fields), records, errors); + } + } +} diff --git a/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Filter.CSV/Filters/TraceCsvInputFilter.cs b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Filter.CSV/Filters/TraceCsvInputFilter.cs new file mode 100644 index 000000000..2ebc529ce --- /dev/null +++ b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Filter.CSV/Filters/TraceCsvInputFilter.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using TraceAnalysis.Base.Filters; +using TraceAnalysis.Base.Models; +using TraceAnalysis.Base.Models.Interfaces; +using TraceAnalysis.Filter.CSV.Model; + +namespace TraceAnalysis.Filter.CSV.Filters +{ + /// + /// Input filter for the TraceCsv format produced by trace-export tools. + /// The format is identified by the header line [key]; [value]. + /// + public class TraceCsvInputFilter : IInputFilter + { + /// Expected first line of a TraceCsv stream. + private const string TraceCsvHeader = "[key]; [value]"; + + /// + /// + /// Detects the format by file extension (.csv) first, + /// then verifies the stream header line. + /// The stream position is restored after inspection. + /// Returns false when the stream does not support seeking. + /// + public bool CanHandle(Stream _stream, string _sourceId) + { + if (!_stream.CanSeek) + return false; + + var ext = System.IO.Path.GetExtension(_sourceId); + if (!string.IsNullOrEmpty(ext) && !ext.Equals(".csv", StringComparison.OrdinalIgnoreCase)) + return false; + + var startPos = _stream.Position; + try + { + using var reader = new StreamReader(_stream, Encoding.UTF8, true, 1024, leaveOpen: true); + var firstLine = reader.ReadLine(); + return firstLine == TraceCsvHeader; + } + finally + { + _stream.Position = startPos; + } + } + + /// + /// + /// Reads the TraceCsv stream into a and converts it + /// to the canonical structure. + /// The first column (TimeBase) is used as the mandatory timestamp. + /// All other columns are treated as optional double fields. + /// Parse errors are collected in . + /// + public ITraceDataSet Read(Stream _stream, string _sourceId) + { + var errors = new List(); + var model = new CsvModel(); + + try + { + model.ReadTraceCSV(_stream); + } + catch (Exception ex) + { + errors.Add($"Failed to read TraceCsv from '{_sourceId}': {ex.Message}"); + return new TraceDataSet( + new TraceMetadata(_sourceId, Array.Empty()), + Array.Empty(), + errors); + } + + // Build field metadata from header, skipping the first column (TimeBase = timestamp key). + var fields = new List(); + for (var i = 1; i < model.Header.Count; i++) + fields.Add(new TraceFieldMetadata(model.Header[i].name, model.Header[i].type)); + + // Convert each row to a canonical record. + var records = new List(); + for (var i = 0; i < model.Rows.Count; i++) + { + var row = model.Rows[i]; + var timestamp = row.TryGetValue("TimeBase", out var ts) ? ts : (object)i; + var values = new Dictionary(); + foreach (var kvp in row) + if (kvp.Key != "TimeBase") + values[kvp.Key] = kvp.Value; + + records.Add(new TraceRecord(timestamp, values)); + } + + return new TraceDataSet(new TraceMetadata(_sourceId, fields), records, errors); + } + } +} diff --git a/CSharpBible/Data/TraceCsv2realCsv/Model/CsvModel.cs b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Filter.CSV/Model/CsvModel.cs similarity index 96% rename from CSharpBible/Data/TraceCsv2realCsv/Model/CsvModel.cs rename to CSharpBible/Data/TraceAnalysis/TraceAnalysis.Filter.CSV/Model/CsvModel.cs index d306e6bbf..a742112d5 100644 --- a/CSharpBible/Data/TraceCsv2realCsv/Model/CsvModel.cs +++ b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Filter.CSV/Model/CsvModel.cs @@ -1,232 +1,235 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using BaseLib.Helper; - -namespace TraceCsv2realCsv.Model -{ - public class CsvModel - { - #region internal Class - public class _DataRows - { - CsvModel _parent; - public int Count => _parent._data.Count; - - public int Fields => _parent._header.Count; - - public Dictionary this[int ix] - { - get - { - var val = new Dictionary(); - var row = _parent._data.ElementAt(ix); - var i = 0; - val[_parent._header[i++].name] = row.Key; - foreach (var fieldval in _parent._data.ElementAt(ix).Value) - val[_parent._header[i++].name] = fieldval; - return val; - } - } - - public _DataRows(CsvModel parent) - => _parent = parent; - } - #endregion - #region Properties - // Header - private List<(string name, Type? type)> _header = new(); - // Data - private Dictionary> _data = new(); - private List _separators = new() { ";", "\t", "," }; - const char cQuotation = '\"'; - public _DataRows Rows { get; } - #endregion - - #region Methods - public CsvModel() - { - Rows = new _DataRows(this); - } - public bool ReadCsv(Stream st) - => ReadCsv(st, _separators); - - public bool ReadCsv(Stream st, List separators) - { - using var tr = new StreamReader(st, true); - // Header - var line = tr.ReadLine(); - var seperator = _separators.FirstOrDefault(s => line.Contains(s)) ?? ","; - _header.Clear(); - var header = line.Split(new string[] { seperator }, StringSplitOptions.None).ToList(); - - // Lesen Sie die ersten fünf Zeilen der CSV-Datei und trennen Sie die Werte. - var lines = new List>(); - for (int i = 0; i < 5 && !tr.EndOfStream; i++) - { - lines.Add(SplitCSVLine(tr.ReadLine(), seperator, cQuotation)); - } - - // Bestimmen Sie den Datentyp jeder Spalte. - for (int i = 0; i < header.Count; i++) - { - _header.Add((header[i], GetColumnType(lines.Select(l => l[i]).ToList()))); - } - - foreach (var lnVals in lines) - AppendData(CastList(lnVals)); - - while (!tr.EndOfStream) - { - line = tr.ReadLine(); - var values = SplitCSVLine(line, seperator, cQuotation); - - AppendData(CastList(values)); - } - return true; - } - - public void AppendData(List values) - // _data - { - // Hier können Sie die Werte in Ihre Klasse einfügen. - var Index = values[0]; - values.RemoveAt(0); - _data.Add(Index, values); - } - - public void AppendData(object[][] values) - // _data - { - // Hier können Sie die Werte in Ihre Klasse einfügen. - foreach (object?[] o in values) - AppendData(o.ToList()); - } - public List CastList(List? values) - // Benutzt _header - { - List _List = new(); - for (int i = 0;values != null && i < Math.Min(values.Count, _header.Count); i++) - _List.Add(_header[i].type?.Get(values[i])); - return _List; - } - - public static List SplitCSVLine(string line, string seperator, char quotation = cQuotation) - { - var values = new List(); - int i = 0; - int pn; - if (string.IsNullOrEmpty(seperator)) - return new() { line }; - string value = ""; - - while (i < line.Length) - { - if (line.Substring(i).StartsWith(seperator)) - { - values.Add(value); - i += seperator.Length; - value = ""; - if (i == line.Length) - values.Add(value); - } - else if ((line[i] == quotation) - && (((pn = line.IndexOf($"{quotation}" + seperator, i)) > -1) - || (line[pn = line.Length - 1] == quotation))) - { - value = line.Substring(i, pn - i + 1); - i = pn + 1; // length of quotation - if (i == line.Length) - values.Add(value); - } - else if ((line[i] != quotation) - && (pn = line.IndexOf(seperator, i)) > -1) - { - value = line.Substring(i, pn - i); - i = pn; - } - else - { - values.Add(line.Substring(i)); - i = line.Length; - } - } - return values; - } - - public static Type GetColumnType(List values, char quotation = cQuotation) - { - // Versuchen Sie zuerst, Mit den Qutations die Strings herauszufiltern. -#if NET5_0_OR_GREATER - if (values.All(v => v.StartsWith(quotation) && v.EndsWith(quotation))) -#else - if (values.All(v => v.StartsWith(quotation.ToString()) && v.EndsWith(quotation.ToString()))) -#endif - return typeof(string); - else - // Dann versuchen Sie, den Datentyp als Ganzzahl zu bestimmen. - if (values.All(v => int.TryParse(v, out _))) - { - return typeof(int); - } - - // Versuchen Sie dann, den Datentyp als Gleitkommazahl zu bestimmen. - else if (values.All(v => double.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out _))) - { - return typeof(double); - } - - // Wenn alle Werte Zeichenfolgen sind oder nicht in einen Ganzzahl- oder Gleitkommazahl-Datentyp konvertiert werden können, - // geben Sie den Datentyp als Zeichenfolge zurück. - else - return typeof(string); - } - - public void SetHeader(List lsHNames) - { - _data.Clear(); - _header.Clear(); - foreach (string s in lsHNames) - _header.Add((s, typeof(object))); - } - public void SetHeader(List<(string , Type?)> lsHeader) - { - _data.Clear(); - _header.Clear(); - _header = lsHeader; - } - - public void WriteCSV(Stream stream, char cSeparator = '\t') - { - using var tw = new StreamWriter(stream, Encoding.UTF8); - // Write Header - tw.WriteLine(string.Join($"{cSeparator}", _header.ConvertAll((l) => $"\"{l.name}\""))); - // Write Data - foreach (var l in _data) - tw.WriteLine($"{(l.Key is string sl ? $"\"{sl.Quote()}\"" : l.Key)}{cSeparator}" + string.Join($"{cSeparator}", l.Value.ConvertAll((l) => l is string sl ? $"\"{sl.Quote()}\"" : l is double d ? $"{d.ToString(CultureInfo.InvariantCulture)}" : $"{l}"))); - - } - - public void AddColumnData(string header, Dictionary? data) - { - var icolidx = _header.ConvertAll(s => s.name).IndexOf(header); - if (icolidx > 0 && data !=null) - foreach (var r in data) - if (_data.TryGetValue(r.Key, out var ls)) - ls[icolidx - 1] = r.Value; - else - { - ls = new(); - foreach (var h in _header) - if (h.name != _header[0].name) - ls.Add(0d); - ls[icolidx - 1] = r.Value; - _data.Add(r.Key, ls); - } - } - #endregion - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using BaseLib.Helper; + +namespace TraceAnalysis.Filter.CSV.Model +{ + public class CsvModel + { + #region internal Class + public class _DataRows + { + CsvModel _parent; + public int Count => _parent._data.Count; + + public int Fields => _parent._header.Count; + + public Dictionary this[int ix] + { + get + { + var val = new Dictionary(); + var row = _parent._data.ElementAt(ix); + var i = 0; + val[_parent._header[i++].name] = row.Key; + foreach (var fieldval in _parent._data.ElementAt(ix).Value) + val[_parent._header[i++].name] = fieldval; + return val; + } + } + + public _DataRows(CsvModel parent) + => _parent = parent; + } + #endregion + #region Properties + // Header + private List<(string name, Type? type)> _header = new(); + // Data + private Dictionary> _data = new(); + private List _separators = new() { ";", "\t", "," }; + const char cQuotation = '\"'; + public _DataRows Rows { get; } + + /// Returns the ordered list of (name, type) header entries for all columns. + public IReadOnlyList<(string name, Type? type)> Header => _header; + #endregion + + #region Methods + public CsvModel() + { + Rows = new _DataRows(this); + } + public bool ReadCsv(Stream st) + => ReadCsv(st, _separators); + + public bool ReadCsv(Stream st, List separators) + { + using var tr = new StreamReader(st, true); + // Header + var line = tr.ReadLine(); + var seperator = _separators.FirstOrDefault(s => line.Contains(s)) ?? ","; + _header.Clear(); + var header = line.Split(new string[] { seperator }, StringSplitOptions.None).ToList(); + + // Lesen Sie die ersten fünf Zeilen der CSV-Datei und trennen Sie die Werte. + var lines = new List>(); + for (int i = 0; i < 5 && !tr.EndOfStream; i++) + { + lines.Add(SplitCSVLine(tr.ReadLine(), seperator, cQuotation)); + } + + // Bestimmen Sie den Datentyp jeder Spalte. + for (int i = 0; i < header.Count; i++) + { + _header.Add((header[i], GetColumnType(lines.Select(l => l[i]).ToList()))); + } + + foreach (var lnVals in lines) + AppendData(CastList(lnVals)); + + while (!tr.EndOfStream) + { + line = tr.ReadLine(); + var values = SplitCSVLine(line, seperator, cQuotation); + + AppendData(CastList(values)); + } + return true; + } + + public void AppendData(List values) + // _data + { + // Hier können Sie die Werte in Ihre Klasse einfügen. + var Index = values[0]; + values.RemoveAt(0); + _data.Add(Index, values); + } + + public void AppendData(object[][] values) + // _data + { + // Hier können Sie die Werte in Ihre Klasse einfügen. + foreach (object?[] o in values) + AppendData(o.ToList()); + } + public List CastList(List? values) + // Benutzt _header + { + List _List = new(); + for (int i = 0;values != null && i < Math.Min(values.Count, _header.Count); i++) + _List.Add(_header[i].type?.Get(values[i])); + return _List; + } + + public static List SplitCSVLine(string line, string seperator, char quotation = cQuotation) + { + var values = new List(); + int i = 0; + int pn; + if (string.IsNullOrEmpty(seperator)) + return new() { line }; + string value = ""; + + while (i < line.Length) + { + if (line.Substring(i).StartsWith(seperator)) + { + values.Add(value); + i += seperator.Length; + value = ""; + if (i == line.Length) + values.Add(value); + } + else if ((line[i] == quotation) + && (((pn = line.IndexOf($"{quotation}" + seperator, i)) > -1) + || (line[pn = line.Length - 1] == quotation))) + { + value = line.Substring(i, pn - i + 1); + i = pn + 1; // length of quotation + if (i == line.Length) + values.Add(value); + } + else if ((line[i] != quotation) + && (pn = line.IndexOf(seperator, i)) > -1) + { + value = line.Substring(i, pn - i); + i = pn; + } + else + { + values.Add(line.Substring(i)); + i = line.Length; + } + } + return values; + } + + public static Type GetColumnType(List values, char quotation = cQuotation) + { + // Versuchen Sie zuerst, Mit den Qutations die Strings herauszufiltern. +#if NET5_0_OR_GREATER + if (values.All(v => v.StartsWith(quotation) && v.EndsWith(quotation))) +#else + if (values.All(v => v.StartsWith(quotation.ToString()) && v.EndsWith(quotation.ToString()))) +#endif + return typeof(string); + else + // Dann versuchen Sie, den Datentyp als Ganzzahl zu bestimmen. + if (values.All(v => int.TryParse(v, out _))) + { + return typeof(int); + } + + // Versuchen Sie dann, den Datentyp als Gleitkommazahl zu bestimmen. + else if (values.All(v => double.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out _))) + { + return typeof(double); + } + + // Wenn alle Werte Zeichenfolgen sind oder nicht in einen Ganzzahl- oder Gleitkommazahl-Datentyp konvertiert werden können, + // geben Sie den Datentyp als Zeichenfolge zurück. + else + return typeof(string); + } + + public void SetHeader(List lsHNames) + { + _data.Clear(); + _header.Clear(); + foreach (string s in lsHNames) + _header.Add((s, typeof(object))); + } + public void SetHeader(List<(string , Type?)> lsHeader) + { + _data.Clear(); + _header.Clear(); + _header = lsHeader; + } + + public void WriteCSV(Stream stream, char cSeparator = '\t') + { + using var tw = new StreamWriter(stream, Encoding.UTF8); + // Write Header + tw.WriteLine(string.Join($"{cSeparator}", _header.ConvertAll((l) => $"\"{l.name}\""))); + // Write Data + foreach (var l in _data) + tw.WriteLine($"{(l.Key is string sl ? $"\"{sl.Quote()}\"" : l.Key)}{cSeparator}" + string.Join($"{cSeparator}", l.Value.ConvertAll((l) => l is string sl ? $"\"{sl.Quote()}\"" : l is double d ? $"{d.ToString(CultureInfo.InvariantCulture)}" : $"{l}"))); + + } + + public void AddColumnData(string header, Dictionary? data) + { + var icolidx = _header.ConvertAll(s => s.name).IndexOf(header); + if (icolidx > 0 && data !=null) + foreach (var r in data) + if (_data.TryGetValue(r.Key, out var ls)) + ls[icolidx - 1] = r.Value; + else + { + ls = new(); + foreach (var h in _header) + if (h.name != _header[0].name) + ls.Add(0d); + ls[icolidx - 1] = r.Value; + _data.Add(r.Key, ls); + } + } + #endregion + } +} diff --git a/CSharpBible/Data/TraceCsv2realCsv/Model/TraceCSVReader.cs b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Filter.CSV/Model/TraceCSVReader.cs similarity index 95% rename from CSharpBible/Data/TraceCsv2realCsv/Model/TraceCSVReader.cs rename to CSharpBible/Data/TraceAnalysis/TraceAnalysis.Filter.CSV/Model/TraceCSVReader.cs index 21d7ca11f..8bad9157d 100644 --- a/CSharpBible/Data/TraceCsv2realCsv/Model/TraceCSVReader.cs +++ b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Filter.CSV/Model/TraceCSVReader.cs @@ -1,66 +1,66 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; - -namespace TraceCsv2realCsv.Model -{ - public static class TraceCSVReader - { - public static void ReadTraceCSV(this CsvModel model, Stream stream) - { - using (var reader = new StreamReader(stream)) - { - // Check Header - var line = reader.ReadLine(); - - if (line != "[key]; [value]") - throw new ArgumentException("Stream does not contain a Trace-csv"); - - List<(string header, Dictionary? data)> columns = new() {("TimeBase",null) }; - while (!reader.EndOfStream) - { - if (ReadColumn(reader, out var header,out var data)) - columns.Add((header, data)); - } - - model.SetHeader(columns.ConvertAll((s) => (s.header, (Type?)typeof(double)))); - - foreach (var s in columns) - model.AddColumnData(s.header, s.data); - } - } - - private static bool ReadColumn(StreamReader reader, out string header,out Dictionary data) - { - var xColumnEnd = false; - header = ""; - bool xDataMode = false; - data = new(); - while (!reader.EndOfStream && !xColumnEnd ) - { - int pp = reader.Peek(); - if (xDataMode && (pp != 59)) - return !string.IsNullOrEmpty(header); - var line = reader.ReadLine(); - - if (string.IsNullOrEmpty(header) && line.Contains(".Variable;")) - { - header = line.Split(';')[1].Trim(); - } - - if (!xDataMode) - xDataMode = line.TrimEnd().EndsWith(".Data;"); - else - { - var ls = line.Split(';'); - if ((ls.Length > 2) - && int.TryParse(ls[1].Trim(), out var ix) - && double.TryParse(ls[2].Trim(), System.Globalization.NumberStyles.Float, CultureInfo.InvariantCulture, out var dVal)) - data[ix] = dVal; - } - } - return !string.IsNullOrEmpty(header) && xDataMode; - } - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; + +namespace TraceAnalysis.Filter.CSV.Model +{ + public static class TraceCSVReader + { + public static void ReadTraceCSV(this CsvModel model, Stream stream) + { + using (var reader = new StreamReader(stream)) + { + // Check Header + var line = reader.ReadLine(); + + if (line != "[key]; [value]") + throw new ArgumentException("Stream does not contain a Trace-csv"); + + List<(string header, Dictionary? data)> columns = new() {("TimeBase",null) }; + while (!reader.EndOfStream) + { + if (ReadColumn(reader, out var header,out var data)) + columns.Add((header, data)); + } + + model.SetHeader(columns.ConvertAll((s) => (s.header, (Type?)typeof(double)))); + + foreach (var s in columns) + model.AddColumnData(s.header, s.data); + } + } + + private static bool ReadColumn(StreamReader reader, out string header,out Dictionary data) + { + var xColumnEnd = false; + header = ""; + bool xDataMode = false; + data = new(); + while (!reader.EndOfStream && !xColumnEnd ) + { + int pp = reader.Peek(); + if (xDataMode && (pp != 59)) + return !string.IsNullOrEmpty(header); + var line = reader.ReadLine(); + + if (string.IsNullOrEmpty(header) && line.Contains(".Variable;")) + { + header = line.Split(';')[1].Trim(); + } + + if (!xDataMode) + xDataMode = line.TrimEnd().EndsWith(".Data;"); + else + { + var ls = line.Split(';'); + if ((ls.Length > 2) + && int.TryParse(ls[1].Trim(), out var ix) + && double.TryParse(ls[2].Trim(), System.Globalization.NumberStyles.Float, CultureInfo.InvariantCulture, out var dVal)) + data[ix] = dVal; + } + } + return !string.IsNullOrEmpty(header) && xDataMode; + } + } +} diff --git a/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Filter.CSV/TraceAnalysis.Filter.CSV.csproj b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Filter.CSV/TraceAnalysis.Filter.CSV.csproj new file mode 100644 index 000000000..36057463f --- /dev/null +++ b/CSharpBible/Data/TraceAnalysis/TraceAnalysis.Filter.CSV/TraceAnalysis.Filter.CSV.csproj @@ -0,0 +1,22 @@ + + + + Library + net462;net472;net48;net481;net6.0;net7.0;net8.0 + + + + + $(TargetFrameworks);net9.0 + + + $(TargetFrameworks);net10.0 + + + + + + + + + diff --git a/CSharpBible/Data/TraceCsv2realCsv/Program.cs b/CSharpBible/Data/TraceCsv2realCsv/Program.cs index 3dbbc9c23..bd066a082 100644 --- a/CSharpBible/Data/TraceCsv2realCsv/Program.cs +++ b/CSharpBible/Data/TraceCsv2realCsv/Program.cs @@ -1,5 +1,5 @@ using System.IO; -using TraceCsv2realCsv.Model; +using TraceAnalysis.Filter.CSV.Model; namespace TraceCsv2realCsv { diff --git a/CSharpBible/Data/TraceCsv2realCsv/TraceCsv2realCsv.csproj b/CSharpBible/Data/TraceCsv2realCsv/TraceCsv2realCsv.csproj index 89a828058..5576e5da0 100644 --- a/CSharpBible/Data/TraceCsv2realCsv/TraceCsv2realCsv.csproj +++ b/CSharpBible/Data/TraceCsv2realCsv/TraceCsv2realCsv.csproj @@ -16,7 +16,11 @@ - + + + + + diff --git a/CSharpBible/Data/TraceCsv2realCsvTests/Model/CsvModelTests.cs b/CSharpBible/Data/TraceCsv2realCsvTests/Model/CsvModelTests.cs index c5cf975aa..de24921d5 100644 --- a/CSharpBible/Data/TraceCsv2realCsvTests/Model/CsvModelTests.cs +++ b/CSharpBible/Data/TraceCsv2realCsvTests/Model/CsvModelTests.cs @@ -5,6 +5,7 @@ using System.Text; using System.IO; using BaseLib.Helper; +using TraceAnalysis.Filter.CSV.Model; namespace TraceCsv2realCsv.Model.Tests { diff --git a/CSharpBible/Data/TraceCsv2realCsvTests/TraceCsv2realCsvTests.csproj b/CSharpBible/Data/TraceCsv2realCsvTests/TraceCsv2realCsvTests.csproj index 5e7c8b5d6..ccce73ce3 100644 --- a/CSharpBible/Data/TraceCsv2realCsvTests/TraceCsv2realCsvTests.csproj +++ b/CSharpBible/Data/TraceCsv2realCsvTests/TraceCsv2realCsvTests.csproj @@ -1,14 +1,14 @@  - net462;net472;net48;net481;net6.0;net7.0;net8.0;net9.0 + net462;net472;net48;net481;net6.0;net7.0;net8.0 true - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -17,6 +17,8 @@ + + diff --git a/CSharpBible/DependencyInjection/CustomerRepositoryTests/CustomerRepositoryTests.csproj b/CSharpBible/DependencyInjection/CustomerRepositoryTests/CustomerRepositoryTests.csproj index b3a111ddf..0405a7a47 100644 --- a/CSharpBible/DependencyInjection/CustomerRepositoryTests/CustomerRepositoryTests.csproj +++ b/CSharpBible/DependencyInjection/CustomerRepositoryTests/CustomerRepositoryTests.csproj @@ -15,7 +15,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/Games/AsteroidsModernEngine.Tests/AsteroidsModernEngine.Tests.csproj b/CSharpBible/Games/AsteroidsModernEngine.Tests/AsteroidsModernEngine.Tests.csproj index 4cd9f96aa..c28dac82c 100644 --- a/CSharpBible/Games/AsteroidsModernEngine.Tests/AsteroidsModernEngine.Tests.csproj +++ b/CSharpBible/Games/AsteroidsModernEngine.Tests/AsteroidsModernEngine.Tests.csproj @@ -32,7 +32,7 @@ - - + + \ No newline at end of file diff --git a/CSharpBible/Games/DetectiveGame.Tests/DetectiveGame.Tests.csproj b/CSharpBible/Games/DetectiveGame.Tests/DetectiveGame.Tests.csproj index 17c22cb15..1b35aea7e 100644 --- a/CSharpBible/Games/DetectiveGame.Tests/DetectiveGame.Tests.csproj +++ b/CSharpBible/Games/DetectiveGame.Tests/DetectiveGame.Tests.csproj @@ -23,7 +23,7 @@ - - + + \ No newline at end of file diff --git a/CSharpBible/Games/Galaxia_BaseTests/Galaxia_BaseTests.csproj b/CSharpBible/Games/Galaxia_BaseTests/Galaxia_BaseTests.csproj index a994bb297..bca5abdf8 100644 --- a/CSharpBible/Games/Galaxia_BaseTests/Galaxia_BaseTests.csproj +++ b/CSharpBible/Games/Galaxia_BaseTests/Galaxia_BaseTests.csproj @@ -23,8 +23,8 @@ - - + + diff --git a/CSharpBible/Games/Galaxia_UI.Tests/Galaxia_UI.Tests.csproj b/CSharpBible/Games/Galaxia_UI.Tests/Galaxia_UI.Tests.csproj index f021ee2da..6df1cf0d0 100644 --- a/CSharpBible/Games/Galaxia_UI.Tests/Galaxia_UI.Tests.csproj +++ b/CSharpBible/Games/Galaxia_UI.Tests/Galaxia_UI.Tests.csproj @@ -14,6 +14,6 @@ - + \ No newline at end of file diff --git a/CSharpBible/Games/Game_BaseTests/Game_BaseTests.csproj b/CSharpBible/Games/Game_BaseTests/Game_BaseTests.csproj index d4819430a..de9e95043 100644 --- a/CSharpBible/Games/Game_BaseTests/Game_BaseTests.csproj +++ b/CSharpBible/Games/Game_BaseTests/Game_BaseTests.csproj @@ -22,8 +22,8 @@ - - + + diff --git a/CSharpBible/Games/RemoteTerminal.Tests/RemoteTerminal.Tests.csproj b/CSharpBible/Games/RemoteTerminal.Tests/RemoteTerminal.Tests.csproj index 73433b0d9..a5ccb602d 100644 --- a/CSharpBible/Games/RemoteTerminal.Tests/RemoteTerminal.Tests.csproj +++ b/CSharpBible/Games/RemoteTerminal.Tests/RemoteTerminal.Tests.csproj @@ -25,7 +25,7 @@ - + diff --git a/CSharpBible/Games/Snake_BaseTests/Snake_BaseTests.csproj b/CSharpBible/Games/Snake_BaseTests/Snake_BaseTests.csproj index cc23ba46b..6d7ed3bfa 100644 --- a/CSharpBible/Games/Snake_BaseTests/Snake_BaseTests.csproj +++ b/CSharpBible/Games/Snake_BaseTests/Snake_BaseTests.csproj @@ -27,8 +27,8 @@ - - + + diff --git a/CSharpBible/Games/Sokoban_BaseTests/Sokoban_BaseTests.csproj b/CSharpBible/Games/Sokoban_BaseTests/Sokoban_BaseTests.csproj index 717294772..710e1d405 100644 --- a/CSharpBible/Games/Sokoban_BaseTests/Sokoban_BaseTests.csproj +++ b/CSharpBible/Games/Sokoban_BaseTests/Sokoban_BaseTests.csproj @@ -30,8 +30,8 @@ - - + + diff --git a/CSharpBible/Games/Sudoku_BaseTests/Sudoku_BaseTests.csproj b/CSharpBible/Games/Sudoku_BaseTests/Sudoku_BaseTests.csproj index 8023173b8..62d66edd3 100644 --- a/CSharpBible/Games/Sudoku_BaseTests/Sudoku_BaseTests.csproj +++ b/CSharpBible/Games/Sudoku_BaseTests/Sudoku_BaseTests.csproj @@ -43,8 +43,8 @@ - - + + diff --git a/CSharpBible/Games/Tetris_BaseTests/Tetris_BaseTests.csproj b/CSharpBible/Games/Tetris_BaseTests/Tetris_BaseTests.csproj index 62e6b4eb9..641f3a81f 100644 --- a/CSharpBible/Games/Tetris_BaseTests/Tetris_BaseTests.csproj +++ b/CSharpBible/Games/Tetris_BaseTests/Tetris_BaseTests.csproj @@ -30,8 +30,8 @@ - - + + diff --git a/CSharpBible/Games/TileSetAnimator.Tests/TileSetAnimator.Tests.csproj b/CSharpBible/Games/TileSetAnimator.Tests/TileSetAnimator.Tests.csproj index 91f4a927a..c454764b8 100644 --- a/CSharpBible/Games/TileSetAnimator.Tests/TileSetAnimator.Tests.csproj +++ b/CSharpBible/Games/TileSetAnimator.Tests/TileSetAnimator.Tests.csproj @@ -28,7 +28,7 @@ - + diff --git a/CSharpBible/Games/Treppen.BaseTests/Treppen.BaseTests.csproj b/CSharpBible/Games/Treppen.BaseTests/Treppen.BaseTests.csproj index ad5423255..cf92e88d0 100644 --- a/CSharpBible/Games/Treppen.BaseTests/Treppen.BaseTests.csproj +++ b/CSharpBible/Games/Treppen.BaseTests/Treppen.BaseTests.csproj @@ -17,7 +17,7 @@ - - + + diff --git a/CSharpBible/Games/VTileEdit.WPFTests/VTileEdit.WPFTests.csproj b/CSharpBible/Games/VTileEdit.WPFTests/VTileEdit.WPFTests.csproj index 76aa2d48c..d004c73ee 100644 --- a/CSharpBible/Games/VTileEdit.WPFTests/VTileEdit.WPFTests.csproj +++ b/CSharpBible/Games/VTileEdit.WPFTests/VTileEdit.WPFTests.csproj @@ -33,8 +33,8 @@ - - + + diff --git a/CSharpBible/Games/VTileEditTests/VTileEditTests.csproj b/CSharpBible/Games/VTileEditTests/VTileEditTests.csproj index 774bd5815..0663b7699 100644 --- a/CSharpBible/Games/VTileEditTests/VTileEditTests.csproj +++ b/CSharpBible/Games/VTileEditTests/VTileEditTests.csproj @@ -51,7 +51,7 @@ - - + + diff --git a/CSharpBible/Games/Werner_Flaschbier_BaseTests/Werner_Flaschbier_BaseTests.csproj b/CSharpBible/Games/Werner_Flaschbier_BaseTests/Werner_Flaschbier_BaseTests.csproj index 2c24aee13..659059e0a 100644 --- a/CSharpBible/Games/Werner_Flaschbier_BaseTests/Werner_Flaschbier_BaseTests.csproj +++ b/CSharpBible/Games/Werner_Flaschbier_BaseTests/Werner_Flaschbier_BaseTests.csproj @@ -29,8 +29,8 @@ - - + + diff --git a/CSharpBible/Games/Werner_Flaschbier_BaseTests/Werner_Flaschbier_ConsoleTests.csproj b/CSharpBible/Games/Werner_Flaschbier_BaseTests/Werner_Flaschbier_ConsoleTests.csproj index 7709cd3b3..60aceffcf 100644 --- a/CSharpBible/Games/Werner_Flaschbier_BaseTests/Werner_Flaschbier_ConsoleTests.csproj +++ b/CSharpBible/Games/Werner_Flaschbier_BaseTests/Werner_Flaschbier_ConsoleTests.csproj @@ -25,8 +25,8 @@ - - + + diff --git a/CSharpBible/Graphics/MVVM_ImageHandlingTests/MVVM_ImageHandlingTests.csproj b/CSharpBible/Graphics/MVVM_ImageHandlingTests/MVVM_ImageHandlingTests.csproj index f7639ad8a..f8d606615 100644 --- a/CSharpBible/Graphics/MVVM_ImageHandlingTests/MVVM_ImageHandlingTests.csproj +++ b/CSharpBible/Graphics/MVVM_ImageHandlingTests/MVVM_ImageHandlingTests.csproj @@ -10,8 +10,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/Graphics/MVVM_ImageHandlingTests/MVVM_ImageHandling_netTests.csproj b/CSharpBible/Graphics/MVVM_ImageHandlingTests/MVVM_ImageHandling_netTests.csproj index 1f04824af..90d3a5a18 100644 --- a/CSharpBible/Graphics/MVVM_ImageHandlingTests/MVVM_ImageHandling_netTests.csproj +++ b/CSharpBible/Graphics/MVVM_ImageHandlingTests/MVVM_ImageHandling_netTests.csproj @@ -16,8 +16,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/Graphics/MarbleBoard.Engine.Tests/MarbleBoard.Engine.Tests.csproj b/CSharpBible/Graphics/MarbleBoard.Engine.Tests/MarbleBoard.Engine.Tests.csproj index 3a463af98..66d010c57 100644 --- a/CSharpBible/Graphics/MarbleBoard.Engine.Tests/MarbleBoard.Engine.Tests.csproj +++ b/CSharpBible/Graphics/MarbleBoard.Engine.Tests/MarbleBoard.Engine.Tests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/Graphics/PermutationTests/PermutationTests.csproj b/CSharpBible/Graphics/PermutationTests/PermutationTests.csproj index 30fdbd46e..dad68a013 100644 --- a/CSharpBible/Graphics/PermutationTests/PermutationTests.csproj +++ b/CSharpBible/Graphics/PermutationTests/PermutationTests.csproj @@ -13,8 +13,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/Graphics/ScreenX.BaseTests/ScreenX.BaseTests.csproj b/CSharpBible/Graphics/ScreenX.BaseTests/ScreenX.BaseTests.csproj index 56ac400c6..d2495d5d0 100644 --- a/CSharpBible/Graphics/ScreenX.BaseTests/ScreenX.BaseTests.csproj +++ b/CSharpBible/Graphics/ScreenX.BaseTests/ScreenX.BaseTests.csproj @@ -23,7 +23,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/CSharpBible/Libraries/BaseLib/Models/FileProxy.cs b/CSharpBible/Libraries/BaseLib/Models/FileProxy.cs new file mode 100644 index 000000000..09c1ab499 --- /dev/null +++ b/CSharpBible/Libraries/BaseLib/Models/FileProxy.cs @@ -0,0 +1,39 @@ +using BaseLib.Models.Interfaces; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BaseLib.Models; + +public class FileProxy : IFile +{ + public bool Exists(string sPath) + => File.Exists(sPath); + public Stream OpenRead(string sPath) + => File.OpenRead(sPath); + public Stream OpenWrite(string sPath) + => File.OpenWrite(sPath); + public Stream Create(string sPath) + => File.Create(sPath); + public string ReadAllText(string sPath) + => File.ReadAllText(sPath); + public string ReadAllText(string sPath, Encoding encoding) + => File.ReadAllText(sPath, encoding); + public void WriteAllText(string sPath, string sContents) + => File.WriteAllText(sPath, sContents); + public void WriteAllText(string sPath, string sContents, Encoding encoding) + => File.WriteAllText(sPath, sContents, encoding); + public byte[] ReadAllBytes(string sPath) + => File.ReadAllBytes(sPath); + public void WriteAllBytes(string sPath, byte[] rgBytes) + => File.WriteAllBytes(sPath, rgBytes); + public void Delete(string sPath) + => File.Delete(sPath); + public void Copy(string sSourceFileName, string sDestFileName, bool xOverwrite) + => File.Copy(sSourceFileName, sDestFileName, xOverwrite); + public void Move(string sSourceFileName, string sDestFileName) + => File.Move(sSourceFileName, sDestFileName); +} diff --git a/CSharpBible/Libraries/BaseLib/Models/Interfaces/IFile.cs b/CSharpBible/Libraries/BaseLib/Models/Interfaces/IFile.cs new file mode 100644 index 000000000..eadf02be8 --- /dev/null +++ b/CSharpBible/Libraries/BaseLib/Models/Interfaces/IFile.cs @@ -0,0 +1,103 @@ +using System.IO; +using System.Text; + +namespace BaseLib.Models.Interfaces; + +/// +/// Provides an abstraction over for testable file system access. +/// +public interface IFile +{ + /// + /// Determines whether the specified file exists. + /// + /// The file path. + /// if the file exists; otherwise . + bool Exists(string sPath); + + /// + /// Opens a file for reading. + /// + /// The file path. + /// A readable stream. + Stream OpenRead(string sPath); + + /// + /// Opens an existing file for writing. + /// + /// The file path. + /// A writable stream. + Stream OpenWrite(string sPath); + + /// + /// Creates or overwrites a file. + /// + /// The file path. + /// A writable stream. + Stream Create(string sPath); + + /// + /// Reads all text from a file using UTF-8 encoding. + /// + /// The file path. + /// The file content. + string ReadAllText(string sPath); + + /// + /// Reads all text from a file using the specified encoding. + /// + /// The file path. + /// The text encoding. + /// The file content. + string ReadAllText(string sPath, Encoding encoding); + + /// + /// Writes text to a file using UTF-8 encoding. + /// + /// The file path. + /// The text content. + void WriteAllText(string sPath, string sContents); + + /// + /// Writes text to a file using the specified encoding. + /// + /// The file path. + /// The text content. + /// The text encoding. + void WriteAllText(string sPath, string sContents, Encoding encoding); + + /// + /// Reads all bytes from a file. + /// + /// The file path. + /// The file content as bytes. + byte[] ReadAllBytes(string sPath); + + /// + /// Writes all bytes to a file. + /// + /// The file path. + /// The byte content. + void WriteAllBytes(string sPath, byte[] rgBytes); + + /// + /// Deletes the specified file. + /// + /// The file path. + void Delete(string sPath); + + /// + /// Copies a file to a new location. + /// + /// The source file path. + /// The destination file path. + /// to overwrite an existing destination file. + void Copy(string sSourceFileName, string sDestFileName, bool xOverwrite); + + /// + /// Moves a file to a new location. + /// + /// The source file path. + /// The destination file path. + void Move(string sSourceFileName, string sDestFileName); +} diff --git a/CSharpBible/Libraries/BaseLibTests/BaseLibTests.csproj b/CSharpBible/Libraries/BaseLibTests/BaseLibTests.csproj index 1f48d0795..a0d0961c3 100644 --- a/CSharpBible/Libraries/BaseLibTests/BaseLibTests.csproj +++ b/CSharpBible/Libraries/BaseLibTests/BaseLibTests.csproj @@ -15,7 +15,7 @@ $(TargetFrameworks);net10.0 - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/Libraries/ConsoleDisplayTests/ConsoleDisplayTests.csproj b/CSharpBible/Libraries/ConsoleDisplayTests/ConsoleDisplayTests.csproj index 5653aafcd..bdb18edaa 100644 --- a/CSharpBible/Libraries/ConsoleDisplayTests/ConsoleDisplayTests.csproj +++ b/CSharpBible/Libraries/ConsoleDisplayTests/ConsoleDisplayTests.csproj @@ -17,7 +17,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/Libraries/ConsoleLibTests/ConsoleLibTests.csproj b/CSharpBible/Libraries/ConsoleLibTests/ConsoleLibTests.csproj index 27bb48a54..1161cfe77 100644 --- a/CSharpBible/Libraries/ConsoleLibTests/ConsoleLibTests.csproj +++ b/CSharpBible/Libraries/ConsoleLibTests/ConsoleLibTests.csproj @@ -14,8 +14,8 @@ - - + + \ No newline at end of file diff --git a/CSharpBible/Libraries/MathLibraryTests/MathLibraryTests.csproj b/CSharpBible/Libraries/MathLibraryTests/MathLibraryTests.csproj index 069fc5b3d..0295358ea 100644 --- a/CSharpBible/Libraries/MathLibraryTests/MathLibraryTests.csproj +++ b/CSharpBible/Libraries/MathLibraryTests/MathLibraryTests.csproj @@ -18,7 +18,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/CSharpBible/MVVM_Tutorial/DemoLibraryTests/DemoLibraryTests.csproj b/CSharpBible/MVVM_Tutorial/DemoLibraryTests/DemoLibraryTests.csproj index 53a0d6951..6856f97f1 100644 --- a/CSharpBible/MVVM_Tutorial/DemoLibraryTests/DemoLibraryTests.csproj +++ b/CSharpBible/MVVM_Tutorial/DemoLibraryTests/DemoLibraryTests.csproj @@ -20,8 +20,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/ItemsControlTut3_netTests/ItemsControlTut3_netTests.csproj b/CSharpBible/MVVM_Tutorial/ItemsControlTut3_netTests/ItemsControlTut3_netTests.csproj index 8a961f260..56698412c 100644 --- a/CSharpBible/MVVM_Tutorial/ItemsControlTut3_netTests/ItemsControlTut3_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/ItemsControlTut3_netTests/ItemsControlTut3_netTests.csproj @@ -21,8 +21,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/ItemsControlTut4_netTests/ItemsControlTut4_netTests.csproj b/CSharpBible/MVVM_Tutorial/ItemsControlTut4_netTests/ItemsControlTut4_netTests.csproj index acfdc656f..4751ea128 100644 --- a/CSharpBible/MVVM_Tutorial/ItemsControlTut4_netTests/ItemsControlTut4_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/ItemsControlTut4_netTests/ItemsControlTut4_netTests.csproj @@ -20,8 +20,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/ListBindingTests/ListBindingTests.csproj b/CSharpBible/MVVM_Tutorial/ListBindingTests/ListBindingTests.csproj index 581f06e4c..35bd199e3 100644 --- a/CSharpBible/MVVM_Tutorial/ListBindingTests/ListBindingTests.csproj +++ b/CSharpBible/MVVM_Tutorial/ListBindingTests/ListBindingTests.csproj @@ -26,8 +26,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/ListBindingTests/ListBinding_netTests.csproj b/CSharpBible/MVVM_Tutorial/ListBindingTests/ListBinding_netTests.csproj index 8fb63e63c..4c9ef99c9 100644 --- a/CSharpBible/MVVM_Tutorial/ListBindingTests/ListBinding_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/ListBindingTests/ListBinding_netTests.csproj @@ -24,8 +24,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_00_IoCTemplateTests/MVVM_00_IoCTemplateTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_00_IoCTemplateTests/MVVM_00_IoCTemplateTests.csproj index 2a1b37dca..7cec8050f 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_00_IoCTemplateTests/MVVM_00_IoCTemplateTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_00_IoCTemplateTests/MVVM_00_IoCTemplateTests.csproj @@ -12,8 +12,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_00_IoCTemplateTests/MVVM_00_IoCTemplate_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_00_IoCTemplateTests/MVVM_00_IoCTemplate_netTests.csproj index 59f7913b9..232e9c733 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_00_IoCTemplateTests/MVVM_00_IoCTemplate_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_00_IoCTemplateTests/MVVM_00_IoCTemplate_netTests.csproj @@ -15,8 +15,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_00_TemplateTests/MVVM_00_TemplateTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_00_TemplateTests/MVVM_00_TemplateTests.csproj index 29197f175..da5601a9d 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_00_TemplateTests/MVVM_00_TemplateTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_00_TemplateTests/MVVM_00_TemplateTests.csproj @@ -8,8 +8,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_00_TemplateTests/MVVM_00_Template_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_00_TemplateTests/MVVM_00_Template_netTests.csproj index 8c8c06e44..1d6dda2fc 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_00_TemplateTests/MVVM_00_Template_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_00_TemplateTests/MVVM_00_Template_netTests.csproj @@ -14,8 +14,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_00a_CTTemplateTests/MVVM_00a_CTTemplateTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_00a_CTTemplateTests/MVVM_00a_CTTemplateTests.csproj index 9dd79fc16..909591af2 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_00a_CTTemplateTests/MVVM_00a_CTTemplateTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_00a_CTTemplateTests/MVVM_00a_CTTemplateTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_00a_CTTemplateTests/MVVM_00a_CTTemplate_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_00a_CTTemplateTests/MVVM_00a_CTTemplate_netTests.csproj index 29a33a664..f3d8ecf27 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_00a_CTTemplateTests/MVVM_00a_CTTemplate_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_00a_CTTemplateTests/MVVM_00a_CTTemplate_netTests.csproj @@ -15,8 +15,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_03_NotifyChangeTests/MVVM_03_NotifyChangeTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_03_NotifyChangeTests/MVVM_03_NotifyChangeTests.csproj index ebf25484a..44d27605d 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_03_NotifyChangeTests/MVVM_03_NotifyChangeTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_03_NotifyChangeTests/MVVM_03_NotifyChangeTests.csproj @@ -8,8 +8,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_03_NotifyChangeTests/MVVM_03_NotifyChange_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_03_NotifyChangeTests/MVVM_03_NotifyChange_netTests.csproj index 7ff845267..8184da9b6 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_03_NotifyChangeTests/MVVM_03_NotifyChange_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_03_NotifyChangeTests/MVVM_03_NotifyChange_netTests.csproj @@ -13,8 +13,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_03a_CTNotifyChangeTests/MVVM_03a_CTNotifyChangeTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_03a_CTNotifyChangeTests/MVVM_03a_CTNotifyChangeTests.csproj index 0ba77f0fb..9f13918fa 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_03a_CTNotifyChangeTests/MVVM_03a_CTNotifyChangeTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_03a_CTNotifyChangeTests/MVVM_03a_CTNotifyChangeTests.csproj @@ -8,8 +8,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_03a_CTNotifyChangeTests/MVVM_03a_CTNotifyChange_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_03a_CTNotifyChangeTests/MVVM_03a_CTNotifyChange_netTests.csproj index 53577cc5f..dc840f9cb 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_03a_CTNotifyChangeTests/MVVM_03a_CTNotifyChange_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_03a_CTNotifyChangeTests/MVVM_03a_CTNotifyChange_netTests.csproj @@ -13,8 +13,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_04_DelegateCommandTests/MVVM_04_DelegateCommandTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_04_DelegateCommandTests/MVVM_04_DelegateCommandTests.csproj index ff507a564..eae2f9920 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_04_DelegateCommandTests/MVVM_04_DelegateCommandTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_04_DelegateCommandTests/MVVM_04_DelegateCommandTests.csproj @@ -8,8 +8,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_04_DelegateCommandTests/MVVM_04_DelegateCommand_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_04_DelegateCommandTests/MVVM_04_DelegateCommand_netTests.csproj index 4e0e78d06..1b0aeb444 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_04_DelegateCommandTests/MVVM_04_DelegateCommand_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_04_DelegateCommandTests/MVVM_04_DelegateCommand_netTests.csproj @@ -13,8 +13,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_04a_CTRelayCommandTests/MVVM_04a_CTRelayCommandTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_04a_CTRelayCommandTests/MVVM_04a_CTRelayCommandTests.csproj index b6444a223..1d01e16f0 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_04a_CTRelayCommandTests/MVVM_04a_CTRelayCommandTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_04a_CTRelayCommandTests/MVVM_04a_CTRelayCommandTests.csproj @@ -8,8 +8,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_04a_CTRelayCommandTests/MVVM_04a_CTRelayCommand_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_04a_CTRelayCommandTests/MVVM_04a_CTRelayCommand_netTests.csproj index a906fe4ea..2a097d795 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_04a_CTRelayCommandTests/MVVM_04a_CTRelayCommand_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_04a_CTRelayCommandTests/MVVM_04a_CTRelayCommand_netTests.csproj @@ -13,8 +13,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_05_CommandParCalculatorTests/MVVM_05_CommandParCalculatorTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_05_CommandParCalculatorTests/MVVM_05_CommandParCalculatorTests.csproj index 0dd2d37ad..7f3b0b9bc 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_05_CommandParCalculatorTests/MVVM_05_CommandParCalculatorTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_05_CommandParCalculatorTests/MVVM_05_CommandParCalculatorTests.csproj @@ -8,8 +8,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_05_CommandParCalculatorTests/MVVM_05_CommandParCalculator_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_05_CommandParCalculatorTests/MVVM_05_CommandParCalculator_netTests.csproj index fb39e7f95..bfee9085b 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_05_CommandParCalculatorTests/MVVM_05_CommandParCalculator_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_05_CommandParCalculatorTests/MVVM_05_CommandParCalculator_netTests.csproj @@ -13,8 +13,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_05a_CTCommandParCalcTests/MVVM_05a_CTCommandParCalcTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_05a_CTCommandParCalcTests/MVVM_05a_CTCommandParCalcTests.csproj index d1bf6e5db..e57a62d57 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_05a_CTCommandParCalcTests/MVVM_05a_CTCommandParCalcTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_05a_CTCommandParCalcTests/MVVM_05a_CTCommandParCalcTests.csproj @@ -8,8 +8,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_05a_CTCommandParCalcTests/MVVM_05a_CTCommandParCalc_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_05a_CTCommandParCalcTests/MVVM_05a_CTCommandParCalc_netTests.csproj index b51ca7009..6db7b76fb 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_05a_CTCommandParCalcTests/MVVM_05a_CTCommandParCalc_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_05a_CTCommandParCalcTests/MVVM_05a_CTCommandParCalc_netTests.csproj @@ -13,8 +13,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_06_ConvertersTests/MVVM_06_ConvertersTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_06_ConvertersTests/MVVM_06_ConvertersTests.csproj index 547cc3677..025751179 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_06_ConvertersTests/MVVM_06_ConvertersTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_06_ConvertersTests/MVVM_06_ConvertersTests.csproj @@ -8,8 +8,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_06_Converters_3Tests/MVVM_06_Converters_3Tests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_06_Converters_3Tests/MVVM_06_Converters_3Tests.csproj index e356ca91d..aa17a4492 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_06_Converters_3Tests/MVVM_06_Converters_3Tests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_06_Converters_3Tests/MVVM_06_Converters_3Tests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_06_Converters_3Tests/MVVM_06_Converters_3_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_06_Converters_3Tests/MVVM_06_Converters_3_netTests.csproj index 9361e7ce9..6cabcb4b6 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_06_Converters_3Tests/MVVM_06_Converters_3_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_06_Converters_3Tests/MVVM_06_Converters_3_netTests.csproj @@ -15,8 +15,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_06_Converters_4Tests/MVVM_06_Converters_4Tests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_06_Converters_4Tests/MVVM_06_Converters_4Tests.csproj index cc47864eb..18eb7682d 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_06_Converters_4Tests/MVVM_06_Converters_4Tests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_06_Converters_4Tests/MVVM_06_Converters_4Tests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_06_Converters_4Tests/MVVM_06_Converters_4_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_06_Converters_4Tests/MVVM_06_Converters_4_netTests.csproj index 58e734a50..475260b60 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_06_Converters_4Tests/MVVM_06_Converters_4_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_06_Converters_4Tests/MVVM_06_Converters_4_netTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_09_DialogBoxesTest/MVVM_09_DialogBoxesTest.csproj b/CSharpBible/MVVM_Tutorial/MVVM_09_DialogBoxesTest/MVVM_09_DialogBoxesTest.csproj index 0575df29b..5f1596cbf 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_09_DialogBoxesTest/MVVM_09_DialogBoxesTest.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_09_DialogBoxesTest/MVVM_09_DialogBoxesTest.csproj @@ -10,8 +10,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_09_DialogBoxesTest/MVVM_09_DialogBoxes_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_09_DialogBoxesTest/MVVM_09_DialogBoxes_netTests.csproj index a9cab3a3f..d631986b8 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_09_DialogBoxesTest/MVVM_09_DialogBoxes_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_09_DialogBoxesTest/MVVM_09_DialogBoxes_netTests.csproj @@ -15,8 +15,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_09a_CTDialogBoxesTests/MVVM_09a_CTDialogBoxesTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_09a_CTDialogBoxesTests/MVVM_09a_CTDialogBoxesTests.csproj index 5a58db3de..9dfb9bca2 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_09a_CTDialogBoxesTests/MVVM_09a_CTDialogBoxesTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_09a_CTDialogBoxesTests/MVVM_09a_CTDialogBoxesTests.csproj @@ -10,8 +10,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_09a_CTDialogBoxesTests/MVVM_09a_CTDialogBoxes_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_09a_CTDialogBoxesTests/MVVM_09a_CTDialogBoxes_netTests.csproj index e41b2379b..98ceb32dd 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_09a_CTDialogBoxesTests/MVVM_09a_CTDialogBoxes_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_09a_CTDialogBoxesTests/MVVM_09a_CTDialogBoxes_netTests.csproj @@ -15,8 +15,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_16_UserControl1Tests/MVVM_16_UserControl1Tests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_16_UserControl1Tests/MVVM_16_UserControl1Tests.csproj index 48bb4ee75..3e78baec7 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_16_UserControl1Tests/MVVM_16_UserControl1Tests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_16_UserControl1Tests/MVVM_16_UserControl1Tests.csproj @@ -11,8 +11,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_16_UserControl1Tests/MVVM_16_UserControl1_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_16_UserControl1Tests/MVVM_16_UserControl1_netTests.csproj index 36df7f521..bfb6f2fd5 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_16_UserControl1Tests/MVVM_16_UserControl1_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_16_UserControl1Tests/MVVM_16_UserControl1_netTests.csproj @@ -17,8 +17,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_17_1_CSV_LadenTests/MVVM_17_1_CSV_LadenTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_17_1_CSV_LadenTests/MVVM_17_1_CSV_LadenTests.csproj index 4a865e481..948675ef8 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_17_1_CSV_LadenTests/MVVM_17_1_CSV_LadenTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_17_1_CSV_LadenTests/MVVM_17_1_CSV_LadenTests.csproj @@ -22,8 +22,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_17_1_CSV_LadenTests/MVVM_17_1_CSV_Laden_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_17_1_CSV_LadenTests/MVVM_17_1_CSV_Laden_netTests.csproj index 19d08efae..3edc9e8d3 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_17_1_CSV_LadenTests/MVVM_17_1_CSV_Laden_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_17_1_CSV_LadenTests/MVVM_17_1_CSV_Laden_netTests.csproj @@ -24,8 +24,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_18_MultiConvertersTests/MVVM_18_MultiConvertersTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_18_MultiConvertersTests/MVVM_18_MultiConvertersTests.csproj index 4dfdd3e42..e178a32fd 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_18_MultiConvertersTests/MVVM_18_MultiConvertersTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_18_MultiConvertersTests/MVVM_18_MultiConvertersTests.csproj @@ -23,8 +23,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_19_FilterListsTests/MVVM_19_FilterListsTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_19_FilterListsTests/MVVM_19_FilterListsTests.csproj index 57a86e938..e6b50b442 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_19_FilterListsTests/MVVM_19_FilterListsTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_19_FilterListsTests/MVVM_19_FilterListsTests.csproj @@ -26,8 +26,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_19_FilterListsTests/MVVM_19_FilterLists_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_19_FilterListsTests/MVVM_19_FilterLists_netTests.csproj index 65ee32328..17d8e8f1a 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_19_FilterListsTests/MVVM_19_FilterLists_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_19_FilterListsTests/MVVM_19_FilterLists_netTests.csproj @@ -24,8 +24,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_20_SysdialogsTests/MVVM_20_SysdialogsTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_20_SysdialogsTests/MVVM_20_SysdialogsTests.csproj index 8c924a34f..058704d8b 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_20_SysdialogsTests/MVVM_20_SysdialogsTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_20_SysdialogsTests/MVVM_20_SysdialogsTests.csproj @@ -14,8 +14,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_20_SysdialogsTests/MVVM_20_Sysdialogs_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_20_SysdialogsTests/MVVM_20_Sysdialogs_netTests.csproj index c0d794a1f..bbb23a921 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_20_SysdialogsTests/MVVM_20_Sysdialogs_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_20_SysdialogsTests/MVVM_20_Sysdialogs_netTests.csproj @@ -19,8 +19,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_20a_CTSysdialogsTests/MVVM_20a_CTSysdialogsTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_20a_CTSysdialogsTests/MVVM_20a_CTSysdialogsTests.csproj index 7e12f7251..d4f8abb29 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_20a_CTSysdialogsTests/MVVM_20a_CTSysdialogsTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_20a_CTSysdialogsTests/MVVM_20a_CTSysdialogsTests.csproj @@ -14,8 +14,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_20a_CTSysdialogsTests/MVVM_20a_CTSysdialogs_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_20a_CTSysdialogsTests/MVVM_20a_CTSysdialogs_netTests.csproj index 936a8d244..f0937d373 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_20a_CTSysdialogsTests/MVVM_20a_CTSysdialogs_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_20a_CTSysdialogsTests/MVVM_20a_CTSysdialogs_netTests.csproj @@ -19,8 +19,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_22_CTWpfCapTests/MVVM_22_CTWpfCapTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_22_CTWpfCapTests/MVVM_22_CTWpfCapTests.csproj index bb1ca4d86..eb40268c7 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_22_CTWpfCapTests/MVVM_22_CTWpfCapTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_22_CTWpfCapTests/MVVM_22_CTWpfCapTests.csproj @@ -21,8 +21,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_22_CTWpfCapTests/MVVM_22_CTWpfCap_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_22_CTWpfCapTests/MVVM_22_CTWpfCap_netTests.csproj index d8c560eee..b9bf8a050 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_22_CTWpfCapTests/MVVM_22_CTWpfCap_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_22_CTWpfCapTests/MVVM_22_CTWpfCap_netTests.csproj @@ -27,8 +27,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_22_WpfCapTests/MVVM_22_WpfCapTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_22_WpfCapTests/MVVM_22_WpfCapTests.csproj index 35af775db..410571ce3 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_22_WpfCapTests/MVVM_22_WpfCapTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_22_WpfCapTests/MVVM_22_WpfCapTests.csproj @@ -21,8 +21,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_22_WpfCapTests/MVVM_22_WpfCap_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_22_WpfCapTests/MVVM_22_WpfCap_netTests.csproj index 37c3f42d0..bdf4511a1 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_22_WpfCapTests/MVVM_22_WpfCap_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_22_WpfCapTests/MVVM_22_WpfCap_netTests.csproj @@ -27,8 +27,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_24_UserControlTests/MVVM_24_UserControlTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_24_UserControlTests/MVVM_24_UserControlTests.csproj index 48ab4f8fa..6ab7878c7 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_24_UserControlTests/MVVM_24_UserControlTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_24_UserControlTests/MVVM_24_UserControlTests.csproj @@ -8,8 +8,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_24_UserControlTests/MVVM_24_UserControl_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_24_UserControlTests/MVVM_24_UserControl_netTests.csproj index 991782059..4b336fd25 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_24_UserControlTests/MVVM_24_UserControl_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_24_UserControlTests/MVVM_24_UserControl_netTests.csproj @@ -14,8 +14,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_24a_CTUserControlTests/MVVM_24a_CTUserControlTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_24a_CTUserControlTests/MVVM_24a_CTUserControlTests.csproj index 21cbd56d3..d4fd2a0bf 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_24a_CTUserControlTests/MVVM_24a_CTUserControlTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_24a_CTUserControlTests/MVVM_24a_CTUserControlTests.csproj @@ -8,8 +8,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_24a_CTUserControlTests/MVVM_24a_CTUserControl_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_24a_CTUserControlTests/MVVM_24a_CTUserControl_netTests.csproj index 95dd0e628..2ae3a25dc 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_24a_CTUserControlTests/MVVM_24a_CTUserControl_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_24a_CTUserControlTests/MVVM_24a_CTUserControl_netTests.csproj @@ -14,8 +14,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_24b_UserControlTests/MVVM_24b_UserControlTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_24b_UserControlTests/MVVM_24b_UserControlTests.csproj index 3d5c67284..b0bac924a 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_24b_UserControlTests/MVVM_24b_UserControlTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_24b_UserControlTests/MVVM_24b_UserControlTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_24b_UserControlTests/MVVM_24b_UserControl_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_24b_UserControlTests/MVVM_24b_UserControl_netTests.csproj index 9e341687b..da64c7de2 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_24b_UserControlTests/MVVM_24b_UserControl_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_24b_UserControlTests/MVVM_24b_UserControl_netTests.csproj @@ -14,8 +14,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_24c_CTUserControlTests/MVVM_24c_CTUserControlTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_24c_CTUserControlTests/MVVM_24c_CTUserControlTests.csproj index af908d4e1..854f1d2ba 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_24c_CTUserControlTests/MVVM_24c_CTUserControlTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_24c_CTUserControlTests/MVVM_24c_CTUserControlTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_24c_CTUserControlTests/MVVM_24c_CTUserControl_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_24c_CTUserControlTests/MVVM_24c_CTUserControl_netTests.csproj index f6598c4bb..27605e42d 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_24c_CTUserControlTests/MVVM_24c_CTUserControl_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_24c_CTUserControlTests/MVVM_24c_CTUserControl_netTests.csproj @@ -14,8 +14,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_25_RichTextEditTests/MVVM_25_RichTextEditTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_25_RichTextEditTests/MVVM_25_RichTextEditTests.csproj index 359adc09d..8408be23a 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_25_RichTextEditTests/MVVM_25_RichTextEditTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_25_RichTextEditTests/MVVM_25_RichTextEditTests.csproj @@ -12,8 +12,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_25_RichTextEditTests/MVVM_25_RichTextEdit_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_25_RichTextEditTests/MVVM_25_RichTextEdit_netTests.csproj index a3531ab17..b2a704fbf 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_25_RichTextEditTests/MVVM_25_RichTextEdit_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_25_RichTextEditTests/MVVM_25_RichTextEdit_netTests.csproj @@ -15,8 +15,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_27_DataGridTests/MVVM_27_DataGridTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_27_DataGridTests/MVVM_27_DataGridTests.csproj index 4ccea3be3..3e5db2ca6 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_27_DataGridTests/MVVM_27_DataGridTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_27_DataGridTests/MVVM_27_DataGridTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_27_DataGridTests/MVVM_27_DataGrid_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_27_DataGridTests/MVVM_27_DataGrid_netTests.csproj index c8e78398f..21447d554 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_27_DataGridTests/MVVM_27_DataGrid_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_27_DataGridTests/MVVM_27_DataGrid_netTests.csproj @@ -14,8 +14,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_28_1_CTDataGridExtTests/MVVM_28_1_CTDataGridExtTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_28_1_CTDataGridExtTests/MVVM_28_1_CTDataGridExtTests.csproj index 773b705a5..0ab47bd6d 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_28_1_CTDataGridExtTests/MVVM_28_1_CTDataGridExtTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_28_1_CTDataGridExtTests/MVVM_28_1_CTDataGridExtTests.csproj @@ -8,8 +8,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_28_1_CTDataGridExtTests/MVVM_28_1_CTDataGridExt_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_28_1_CTDataGridExtTests/MVVM_28_1_CTDataGridExt_netTests.csproj index 11fe0ae4f..fed5e060a 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_28_1_CTDataGridExtTests/MVVM_28_1_CTDataGridExt_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_28_1_CTDataGridExtTests/MVVM_28_1_CTDataGridExt_netTests.csproj @@ -13,8 +13,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_28_1_DataGridExtTests/MVVM_28_1_DataGridExtTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_28_1_DataGridExtTests/MVVM_28_1_DataGridExtTests.csproj index 6553b86aa..349895ddb 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_28_1_DataGridExtTests/MVVM_28_1_DataGridExtTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_28_1_DataGridExtTests/MVVM_28_1_DataGridExtTests.csproj @@ -8,8 +8,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_28_1_DataGridExtTests/MVVM_28_1_DataGridExt_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_28_1_DataGridExtTests/MVVM_28_1_DataGridExt_netTests.csproj index cf5b8647e..3b0b512b6 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_28_1_DataGridExtTests/MVVM_28_1_DataGridExt_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_28_1_DataGridExtTests/MVVM_28_1_DataGridExt_netTests.csproj @@ -13,8 +13,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_28_DataGridTests/MVVM_28_DataGridTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_28_DataGridTests/MVVM_28_DataGridTests.csproj index 8812629b6..784349fb6 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_28_DataGridTests/MVVM_28_DataGridTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_28_DataGridTests/MVVM_28_DataGridTests.csproj @@ -8,8 +8,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_28_DataGridTests/MVVM_28_DataGrid_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_28_DataGridTests/MVVM_28_DataGrid_netTests.csproj index 50e2f6024..95b531d45 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_28_DataGridTests/MVVM_28_DataGrid_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_28_DataGridTests/MVVM_28_DataGrid_netTests.csproj @@ -13,8 +13,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_31_Validation1Tests/MVVM_31_Validation1Tests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_31_Validation1Tests/MVVM_31_Validation1Tests.csproj index 68082acd7..1d202b12c 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_31_Validation1Tests/MVVM_31_Validation1Tests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_31_Validation1Tests/MVVM_31_Validation1Tests.csproj @@ -7,8 +7,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_31_Validation1Tests/MVVM_31_Validation1_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_31_Validation1Tests/MVVM_31_Validation1_netTests.csproj index 4785ab214..b18d01974 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_31_Validation1Tests/MVVM_31_Validation1_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_31_Validation1Tests/MVVM_31_Validation1_netTests.csproj @@ -13,8 +13,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_31_Validation2Tests/MVVM_31_Validation2Tests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_31_Validation2Tests/MVVM_31_Validation2Tests.csproj index 8f9de4130..992943156 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_31_Validation2Tests/MVVM_31_Validation2Tests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_31_Validation2Tests/MVVM_31_Validation2Tests.csproj @@ -7,8 +7,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_31_Validation2Tests/MVVM_31_Validation2_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_31_Validation2Tests/MVVM_31_Validation2_netTests.csproj index 7c21a1218..a4b4748f2 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_31_Validation2Tests/MVVM_31_Validation2_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_31_Validation2Tests/MVVM_31_Validation2_netTests.csproj @@ -13,8 +13,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_31a_CTValidation1Tests/MVVM_31a_CTValidation1Tests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_31a_CTValidation1Tests/MVVM_31a_CTValidation1Tests.csproj index 0a549b66a..ec4afb6a7 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_31a_CTValidation1Tests/MVVM_31a_CTValidation1Tests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_31a_CTValidation1Tests/MVVM_31a_CTValidation1Tests.csproj @@ -7,8 +7,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_31a_CTValidation1Tests/MVVM_31a_CTValidation1_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_31a_CTValidation1Tests/MVVM_31a_CTValidation1_netTests.csproj index aebb0b923..87ce4aede 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_31a_CTValidation1Tests/MVVM_31a_CTValidation1_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_31a_CTValidation1Tests/MVVM_31a_CTValidation1_netTests.csproj @@ -13,8 +13,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_31a_CTValidation2Tests/MVVM_31a_CTValidation2Tests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_31a_CTValidation2Tests/MVVM_31a_CTValidation2Tests.csproj index f9ea6de1f..6df60e9b8 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_31a_CTValidation2Tests/MVVM_31a_CTValidation2Tests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_31a_CTValidation2Tests/MVVM_31a_CTValidation2Tests.csproj @@ -7,8 +7,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_31a_CTValidation2Tests/MVVM_31a_CTValidation2_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_31a_CTValidation2Tests/MVVM_31a_CTValidation2_netTests.csproj index d851a61b7..ff73a7d2b 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_31a_CTValidation2Tests/MVVM_31a_CTValidation2_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_31a_CTValidation2Tests/MVVM_31a_CTValidation2_netTests.csproj @@ -13,8 +13,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_31a_CTValidation3Tests/MVVM_31a_CTValidation3Tests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_31a_CTValidation3Tests/MVVM_31a_CTValidation3Tests.csproj index 94bdce556..66fad33c9 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_31a_CTValidation3Tests/MVVM_31a_CTValidation3Tests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_31a_CTValidation3Tests/MVVM_31a_CTValidation3Tests.csproj @@ -7,8 +7,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_31a_CTValidation3Tests/MVVM_31a_CTValidation3_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_31a_CTValidation3Tests/MVVM_31a_CTValidation3_netTests.csproj index 25b763c27..cfa017fec 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_31a_CTValidation3Tests/MVVM_31a_CTValidation3_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_31a_CTValidation3Tests/MVVM_31a_CTValidation3_netTests.csproj @@ -13,8 +13,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_33_Events_to_CommandsTests/MVVM_33_Events_to_CommandsTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_33_Events_to_CommandsTests/MVVM_33_Events_to_CommandsTests.csproj index 1e5bd2d9e..4cac5edd1 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_33_Events_to_CommandsTests/MVVM_33_Events_to_CommandsTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_33_Events_to_CommandsTests/MVVM_33_Events_to_CommandsTests.csproj @@ -8,8 +8,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_33_Events_to_CommandsTests/MVVM_33_Events_to_Commands_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_33_Events_to_CommandsTests/MVVM_33_Events_to_Commands_netTests.csproj index 9ce5ef1f4..48afe5403 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_33_Events_to_CommandsTests/MVVM_33_Events_to_Commands_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_33_Events_to_CommandsTests/MVVM_33_Events_to_Commands_netTests.csproj @@ -14,8 +14,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_33a_CTEvents_To_CommandsTests/MVVM_33a_CTEvents_To_CommandsTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_33a_CTEvents_To_CommandsTests/MVVM_33a_CTEvents_To_CommandsTests.csproj index 4a51157b2..1f740e06d 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_33a_CTEvents_To_CommandsTests/MVVM_33a_CTEvents_To_CommandsTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_33a_CTEvents_To_CommandsTests/MVVM_33a_CTEvents_To_CommandsTests.csproj @@ -8,8 +8,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_33a_CTEvents_To_CommandsTests/MVVM_33a_CTEvents_To_Commands_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_33a_CTEvents_To_CommandsTests/MVVM_33a_CTEvents_To_Commands_netTests.csproj index 15a4aa2e6..908a1c53e 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_33a_CTEvents_To_CommandsTests/MVVM_33a_CTEvents_To_Commands_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_33a_CTEvents_To_CommandsTests/MVVM_33a_CTEvents_To_Commands_netTests.csproj @@ -14,8 +14,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_34_BindingEventArgsTests/MVVM_34_BindingEventArgsTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_34_BindingEventArgsTests/MVVM_34_BindingEventArgsTests.csproj index 94907d6f1..2506fcd71 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_34_BindingEventArgsTests/MVVM_34_BindingEventArgsTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_34_BindingEventArgsTests/MVVM_34_BindingEventArgsTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_34_BindingEventArgsTests/MVVM_34_BindingEventArgs_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_34_BindingEventArgsTests/MVVM_34_BindingEventArgs_netTests.csproj index ad29cb97f..2c5c162cb 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_34_BindingEventArgsTests/MVVM_34_BindingEventArgs_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_34_BindingEventArgsTests/MVVM_34_BindingEventArgs_netTests.csproj @@ -14,8 +14,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_34a_CTBindingEventArgsTests/MVVM_34a_CTBindingEventArgsTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_34a_CTBindingEventArgsTests/MVVM_34a_CTBindingEventArgsTests.csproj index 993514e27..ecc08ed05 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_34a_CTBindingEventArgsTests/MVVM_34a_CTBindingEventArgsTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_34a_CTBindingEventArgsTests/MVVM_34a_CTBindingEventArgsTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_34a_CTBindingEventArgsTests/MVVM_34a_CTBindingEventArgs_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_34a_CTBindingEventArgsTests/MVVM_34a_CTBindingEventArgs_netTests.csproj index 1b818a002..2ead7b129 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_34a_CTBindingEventArgsTests/MVVM_34a_CTBindingEventArgs_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_34a_CTBindingEventArgsTests/MVVM_34a_CTBindingEventArgs_netTests.csproj @@ -14,8 +14,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_35_CommunityToolkitTests/MVVM_35_CommunityToolkitTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_35_CommunityToolkitTests/MVVM_35_CommunityToolkitTests.csproj index c43eb6aa1..e9668ef22 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_35_CommunityToolkitTests/MVVM_35_CommunityToolkitTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_35_CommunityToolkitTests/MVVM_35_CommunityToolkitTests.csproj @@ -8,8 +8,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_35_CommunityToolkitTests/MVVM_35_CommunityToolkit_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_35_CommunityToolkitTests/MVVM_35_CommunityToolkit_netTests.csproj index 5a89e15eb..0ca316f69 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_35_CommunityToolkitTests/MVVM_35_CommunityToolkit_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_35_CommunityToolkitTests/MVVM_35_CommunityToolkit_netTests.csproj @@ -14,8 +14,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_36_ComToolKtSavesWorkTests/MVVM_36_ComToolKtSavesWorkTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_36_ComToolKtSavesWorkTests/MVVM_36_ComToolKtSavesWorkTests.csproj index 46f57db6b..255ae5feb 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_36_ComToolKtSavesWorkTests/MVVM_36_ComToolKtSavesWorkTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_36_ComToolKtSavesWorkTests/MVVM_36_ComToolKtSavesWorkTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_36_ComToolKtSavesWorkTests/MVVM_36_ComToolKtSavesWork_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_36_ComToolKtSavesWorkTests/MVVM_36_ComToolKtSavesWork_netTests.csproj index 18f6a9cdc..47cad5153 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_36_ComToolKtSavesWorkTests/MVVM_36_ComToolKtSavesWork_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_36_ComToolKtSavesWorkTests/MVVM_36_ComToolKtSavesWork_netTests.csproj @@ -15,8 +15,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_37_TreeViewTests/MVVM_37_TreeViewTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_37_TreeViewTests/MVVM_37_TreeViewTests.csproj index 5bc71e553..8d11204d7 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_37_TreeViewTests/MVVM_37_TreeViewTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_37_TreeViewTests/MVVM_37_TreeViewTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_37_TreeViewTests/MVVM_37_TreeView_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_37_TreeViewTests/MVVM_37_TreeView_netTests.csproj index 905c79d62..4a6377a5f 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_37_TreeViewTests/MVVM_37_TreeView_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_37_TreeViewTests/MVVM_37_TreeView_netTests.csproj @@ -14,8 +14,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_38_CTDependencyInjectionTests/MVVM_38_CTDependencyInjectionTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_38_CTDependencyInjectionTests/MVVM_38_CTDependencyInjectionTests.csproj index edcf9ccdc..770b6d9a6 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_38_CTDependencyInjectionTests/MVVM_38_CTDependencyInjectionTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_38_CTDependencyInjectionTests/MVVM_38_CTDependencyInjectionTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_38_CTDependencyInjectionTests/MVVM_38_CTDependencyInjection_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_38_CTDependencyInjectionTests/MVVM_38_CTDependencyInjection_netTests.csproj index a379bac56..7a0effcf1 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_38_CTDependencyInjectionTests/MVVM_38_CTDependencyInjection_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_38_CTDependencyInjectionTests/MVVM_38_CTDependencyInjection_netTests.csproj @@ -15,8 +15,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_39_MultiModelTestTests/MVVM_39_MultiModelTestTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_39_MultiModelTestTests/MVVM_39_MultiModelTestTests.csproj index 947224dfe..99b3fea6a 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_39_MultiModelTestTests/MVVM_39_MultiModelTestTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_39_MultiModelTestTests/MVVM_39_MultiModelTestTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_39_MultiModelTestTests/MVVM_39_MultiModelTest_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_39_MultiModelTestTests/MVVM_39_MultiModelTest_netTests.csproj index b2c947c22..96cd2831f 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_39_MultiModelTestTests/MVVM_39_MultiModelTest_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_39_MultiModelTestTests/MVVM_39_MultiModelTest_netTests.csproj @@ -15,8 +15,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_40_WizzardTests/MVVM_40_WizzardTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_40_WizzardTests/MVVM_40_WizzardTests.csproj index 970204376..c36413f11 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_40_WizzardTests/MVVM_40_WizzardTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_40_WizzardTests/MVVM_40_WizzardTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_40_WizzardTests/MVVM_40_Wizzard_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_40_WizzardTests/MVVM_40_Wizzard_netTests.csproj index e9a14a578..78b753147 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_40_WizzardTests/MVVM_40_Wizzard_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_40_WizzardTests/MVVM_40_Wizzard_netTests.csproj @@ -15,8 +15,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_41_SudokuTests/MVVM_41_SudokuTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_41_SudokuTests/MVVM_41_SudokuTests.csproj index 1ec679375..97dfde379 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_41_SudokuTests/MVVM_41_SudokuTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_41_SudokuTests/MVVM_41_SudokuTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_41_SudokuTests/MVVM_41_Sudoku_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_41_SudokuTests/MVVM_41_Sudoku_netTests.csproj index be49c2ab0..87a8c2343 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_41_SudokuTests/MVVM_41_Sudoku_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_41_SudokuTests/MVVM_41_Sudoku_netTests.csproj @@ -16,8 +16,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_99_SomeIssueTests/MVVM_99_SomeIssueTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_99_SomeIssueTests/MVVM_99_SomeIssueTests.csproj index 4d264360a..052f9edae 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_99_SomeIssueTests/MVVM_99_SomeIssueTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_99_SomeIssueTests/MVVM_99_SomeIssueTests.csproj @@ -12,8 +12,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_99_SomeIssueTests/MVVM_99_SomeIssue_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_99_SomeIssueTests/MVVM_99_SomeIssue_netTests.csproj index 11bbecaaa..b78e8c457 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_99_SomeIssueTests/MVVM_99_SomeIssue_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_99_SomeIssueTests/MVVM_99_SomeIssue_netTests.csproj @@ -15,8 +15,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_AllExamplesTests/MVVM_AllExamplesTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_AllExamplesTests/MVVM_AllExamplesTests.csproj index 4cab9d4ab..c6546d01d 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_AllExamplesTests/MVVM_AllExamplesTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_AllExamplesTests/MVVM_AllExamplesTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/MVVM_AllExamplesTests/MVVM_AllExamples_netTests.csproj b/CSharpBible/MVVM_Tutorial/MVVM_AllExamplesTests/MVVM_AllExamples_netTests.csproj index c2ea54644..25e4b9fd7 100644 --- a/CSharpBible/MVVM_Tutorial/MVVM_AllExamplesTests/MVVM_AllExamples_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/MVVM_AllExamplesTests/MVVM_AllExamples_netTests.csproj @@ -15,8 +15,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/WpfAppTests/WpfAppTests.csproj b/CSharpBible/MVVM_Tutorial/WpfAppTests/WpfAppTests.csproj index 5992556ae..680f7e949 100644 --- a/CSharpBible/MVVM_Tutorial/WpfAppTests/WpfAppTests.csproj +++ b/CSharpBible/MVVM_Tutorial/WpfAppTests/WpfAppTests.csproj @@ -8,8 +8,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/MVVM_Tutorial/WpfAppTests/WpfApp_netTests.csproj b/CSharpBible/MVVM_Tutorial/WpfAppTests/WpfApp_netTests.csproj index 1096b0747..f9b4e40e4 100644 --- a/CSharpBible/MVVM_Tutorial/WpfAppTests/WpfApp_netTests.csproj +++ b/CSharpBible/MVVM_Tutorial/WpfAppTests/WpfApp_netTests.csproj @@ -14,8 +14,8 @@ $(TargetFrameworks);net10.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/Patterns_Tutorial/Pattern_00_TemplateTests/Pattern_00_TemplateTests.csproj b/CSharpBible/Patterns_Tutorial/Pattern_00_TemplateTests/Pattern_00_TemplateTests.csproj index a2cb71ff5..58dc70649 100644 --- a/CSharpBible/Patterns_Tutorial/Pattern_00_TemplateTests/Pattern_00_TemplateTests.csproj +++ b/CSharpBible/Patterns_Tutorial/Pattern_00_TemplateTests/Pattern_00_TemplateTests.csproj @@ -13,8 +13,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/Patterns_Tutorial/Pattern_01_SingletonTests/Pattern_01_SingletonTests.csproj b/CSharpBible/Patterns_Tutorial/Pattern_01_SingletonTests/Pattern_01_SingletonTests.csproj index 75b1c4af6..678b92f20 100644 --- a/CSharpBible/Patterns_Tutorial/Pattern_01_SingletonTests/Pattern_01_SingletonTests.csproj +++ b/CSharpBible/Patterns_Tutorial/Pattern_01_SingletonTests/Pattern_01_SingletonTests.csproj @@ -13,8 +13,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/Patterns_Tutorial/Pattern_02_ObserverTests/Pattern_02_ObserverTests.csproj b/CSharpBible/Patterns_Tutorial/Pattern_02_ObserverTests/Pattern_02_ObserverTests.csproj index ba6b83e4e..19752b3d9 100644 --- a/CSharpBible/Patterns_Tutorial/Pattern_02_ObserverTests/Pattern_02_ObserverTests.csproj +++ b/CSharpBible/Patterns_Tutorial/Pattern_02_ObserverTests/Pattern_02_ObserverTests.csproj @@ -13,8 +13,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/SomeThing/QuineTest/QuineTest.csproj b/CSharpBible/SomeThing/QuineTest/QuineTest.csproj index 3048d1f55..18e31fe7a 100644 --- a/CSharpBible/SomeThing/QuineTest/QuineTest.csproj +++ b/CSharpBible/SomeThing/QuineTest/QuineTest.csproj @@ -8,7 +8,7 @@ - + diff --git a/CSharpBible/SomeThing/SomeThing2Tests/SomeThing2Tests.csproj b/CSharpBible/SomeThing/SomeThing2Tests/SomeThing2Tests.csproj index 5320e54d1..f96afffe2 100644 --- a/CSharpBible/SomeThing/SomeThing2Tests/SomeThing2Tests.csproj +++ b/CSharpBible/SomeThing/SomeThing2Tests/SomeThing2Tests.csproj @@ -10,8 +10,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/SomeThing/SomeThing2aTests/SomeThing2aTests.csproj b/CSharpBible/SomeThing/SomeThing2aTests/SomeThing2aTests.csproj index de42129fe..ae3f2c64f 100644 --- a/CSharpBible/SomeThing/SomeThing2aTests/SomeThing2aTests.csproj +++ b/CSharpBible/SomeThing/SomeThing2aTests/SomeThing2aTests.csproj @@ -10,8 +10,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/Tests/Test.csproj b/CSharpBible/Tests/Test.csproj index c6338e43d..f8a2afec3 100644 --- a/CSharpBible/Tests/Test.csproj +++ b/CSharpBible/Tests/Test.csproj @@ -11,8 +11,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/WPFSamples_2/WPF_AnimationTimingTests/WPF_AnimationTimingTests.csproj b/CSharpBible/WPFSamples_2/WPF_AnimationTimingTests/WPF_AnimationTimingTests.csproj index f542ebbe4..c5045d999 100644 --- a/CSharpBible/WPFSamples_2/WPF_AnimationTimingTests/WPF_AnimationTimingTests.csproj +++ b/CSharpBible/WPFSamples_2/WPF_AnimationTimingTests/WPF_AnimationTimingTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/WPFSamples_2/WPF_AnimationTimingTests/WPF_AnimationTiming_netTests.csproj b/CSharpBible/WPFSamples_2/WPF_AnimationTimingTests/WPF_AnimationTiming_netTests.csproj index ffdcffbfb..d5513a215 100644 --- a/CSharpBible/WPFSamples_2/WPF_AnimationTimingTests/WPF_AnimationTiming_netTests.csproj +++ b/CSharpBible/WPFSamples_2/WPF_AnimationTimingTests/WPF_AnimationTiming_netTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/WPFSamples_2/WPF_Complex_LayoutTests/WPF_Complex_LayoutTests.csproj b/CSharpBible/WPFSamples_2/WPF_Complex_LayoutTests/WPF_Complex_LayoutTests.csproj index 2ea2e28fb..0030f0ab7 100644 --- a/CSharpBible/WPFSamples_2/WPF_Complex_LayoutTests/WPF_Complex_LayoutTests.csproj +++ b/CSharpBible/WPFSamples_2/WPF_Complex_LayoutTests/WPF_Complex_LayoutTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/WPFSamples_2/WPF_Complex_LayoutTests/WPF_Complex_Layout_netTests.csproj b/CSharpBible/WPFSamples_2/WPF_Complex_LayoutTests/WPF_Complex_Layout_netTests.csproj index 9ce443bbd..c882ae619 100644 --- a/CSharpBible/WPFSamples_2/WPF_Complex_LayoutTests/WPF_Complex_Layout_netTests.csproj +++ b/CSharpBible/WPFSamples_2/WPF_Complex_LayoutTests/WPF_Complex_Layout_netTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/WPFSamples_2/WPF_ControlsAndLayoutTests/WPF_ControlsAndLayoutTests.csproj b/CSharpBible/WPFSamples_2/WPF_ControlsAndLayoutTests/WPF_ControlsAndLayoutTests.csproj index a6b311436..2eaf01269 100644 --- a/CSharpBible/WPFSamples_2/WPF_ControlsAndLayoutTests/WPF_ControlsAndLayoutTests.csproj +++ b/CSharpBible/WPFSamples_2/WPF_ControlsAndLayoutTests/WPF_ControlsAndLayoutTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/WPFSamples_2/WPF_ControlsAndLayoutTests/WPF_ControlsAndLayout_netTests.csproj b/CSharpBible/WPFSamples_2/WPF_ControlsAndLayoutTests/WPF_ControlsAndLayout_netTests.csproj index 8ed1f72d9..dd87d0522 100644 --- a/CSharpBible/WPFSamples_2/WPF_ControlsAndLayoutTests/WPF_ControlsAndLayout_netTests.csproj +++ b/CSharpBible/WPFSamples_2/WPF_ControlsAndLayoutTests/WPF_ControlsAndLayout_netTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/WPFSamples_2/WPF_CustomAnimationTests/WPF_CustomAnimationTests.csproj b/CSharpBible/WPFSamples_2/WPF_CustomAnimationTests/WPF_CustomAnimationTests.csproj index 252e6ec08..23051d4ab 100644 --- a/CSharpBible/WPFSamples_2/WPF_CustomAnimationTests/WPF_CustomAnimationTests.csproj +++ b/CSharpBible/WPFSamples_2/WPF_CustomAnimationTests/WPF_CustomAnimationTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/WPFSamples_2/WPF_CustomAnimationTests/WPF_CustomAnimation_netTests.csproj b/CSharpBible/WPFSamples_2/WPF_CustomAnimationTests/WPF_CustomAnimation_netTests.csproj index 7a39994e5..2439beb73 100644 --- a/CSharpBible/WPFSamples_2/WPF_CustomAnimationTests/WPF_CustomAnimation_netTests.csproj +++ b/CSharpBible/WPFSamples_2/WPF_CustomAnimationTests/WPF_CustomAnimation_netTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/WPFSamples_2/WPF_Hello_WorldTests/WPF_Hello_WorldTests.csproj b/CSharpBible/WPFSamples_2/WPF_Hello_WorldTests/WPF_Hello_WorldTests.csproj index ef619de4b..dc2aed35c 100644 --- a/CSharpBible/WPFSamples_2/WPF_Hello_WorldTests/WPF_Hello_WorldTests.csproj +++ b/CSharpBible/WPFSamples_2/WPF_Hello_WorldTests/WPF_Hello_WorldTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/WPFSamples_2/WPF_Hello_WorldTests/WPF_Hello_World_netTests.csproj b/CSharpBible/WPFSamples_2/WPF_Hello_WorldTests/WPF_Hello_World_netTests.csproj index e0961695d..ae26dbf20 100644 --- a/CSharpBible/WPFSamples_2/WPF_Hello_WorldTests/WPF_Hello_World_netTests.csproj +++ b/CSharpBible/WPFSamples_2/WPF_Hello_WorldTests/WPF_Hello_World_netTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/WPFSamples_2/WPF_MasterDetailTests/WPF_MasterDetailTests.csproj b/CSharpBible/WPFSamples_2/WPF_MasterDetailTests/WPF_MasterDetailTests.csproj index dfc87bc07..9adfe3d0d 100644 --- a/CSharpBible/WPFSamples_2/WPF_MasterDetailTests/WPF_MasterDetailTests.csproj +++ b/CSharpBible/WPFSamples_2/WPF_MasterDetailTests/WPF_MasterDetailTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/WPFSamples_2/WPF_MasterDetailTests/WPF_MasterDetail_netTests.csproj b/CSharpBible/WPFSamples_2/WPF_MasterDetailTests/WPF_MasterDetail_netTests.csproj index 21efe77a9..d7a9b467c 100644 --- a/CSharpBible/WPFSamples_2/WPF_MasterDetailTests/WPF_MasterDetail_netTests.csproj +++ b/CSharpBible/WPFSamples_2/WPF_MasterDetailTests/WPF_MasterDetail_netTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/WPFSamples_2/WPF_MoveWindowTests/WPF_MoveWindowTests.csproj b/CSharpBible/WPFSamples_2/WPF_MoveWindowTests/WPF_MoveWindowTests.csproj index 6752e265a..1eca3d569 100644 --- a/CSharpBible/WPFSamples_2/WPF_MoveWindowTests/WPF_MoveWindowTests.csproj +++ b/CSharpBible/WPFSamples_2/WPF_MoveWindowTests/WPF_MoveWindowTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/WPFSamples_2/WPF_MoveWindowTests/WPF_MoveWindow_netTests.csproj b/CSharpBible/WPFSamples_2/WPF_MoveWindowTests/WPF_MoveWindow_netTests.csproj index b790229f5..d5ff05d65 100644 --- a/CSharpBible/WPFSamples_2/WPF_MoveWindowTests/WPF_MoveWindow_netTests.csproj +++ b/CSharpBible/WPFSamples_2/WPF_MoveWindowTests/WPF_MoveWindow_netTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/WPFSamples_2/WPF_Sample_TemplateTests/WPF_Sample_TemplateTests.csproj b/CSharpBible/WPFSamples_2/WPF_Sample_TemplateTests/WPF_Sample_TemplateTests.csproj index 2e0cbb39a..a66617c1c 100644 --- a/CSharpBible/WPFSamples_2/WPF_Sample_TemplateTests/WPF_Sample_TemplateTests.csproj +++ b/CSharpBible/WPFSamples_2/WPF_Sample_TemplateTests/WPF_Sample_TemplateTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/WPFSamples_2/WPF_Sample_TemplateTests/WPF_Sample_Template_netTests.csproj b/CSharpBible/WPFSamples_2/WPF_Sample_TemplateTests/WPF_Sample_Template_netTests.csproj index dc70a1ed5..e9e4063ba 100644 --- a/CSharpBible/WPFSamples_2/WPF_Sample_TemplateTests/WPF_Sample_Template_netTests.csproj +++ b/CSharpBible/WPFSamples_2/WPF_Sample_TemplateTests/WPF_Sample_Template_netTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/WPFSamples_2/WPF_StickyNotesDemoTests/WPF_StickyNotesDemoTests.csproj b/CSharpBible/WPFSamples_2/WPF_StickyNotesDemoTests/WPF_StickyNotesDemoTests.csproj index 20567ab6e..637c74270 100644 --- a/CSharpBible/WPFSamples_2/WPF_StickyNotesDemoTests/WPF_StickyNotesDemoTests.csproj +++ b/CSharpBible/WPFSamples_2/WPF_StickyNotesDemoTests/WPF_StickyNotesDemoTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CSharpBible/WPFSamples_2/WPF_StickyNotesDemoTests/WPF_StickyNotesDemo_netTests.csproj b/CSharpBible/WPFSamples_2/WPF_StickyNotesDemoTests/WPF_StickyNotesDemo_netTests.csproj index 78786028c..2a6e96d39 100644 --- a/CSharpBible/WPFSamples_2/WPF_StickyNotesDemoTests/WPF_StickyNotesDemo_netTests.csproj +++ b/CSharpBible/WPFSamples_2/WPF_StickyNotesDemoTests/WPF_StickyNotesDemo_netTests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive From c7ad0082fe6d36a8e1c33c7430e3b1cea508446f Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:04:03 +0200 Subject: [PATCH 66/96] Calc --- CSharpBible/Calc/Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CSharpBible/Calc/Directory.Packages.props b/CSharpBible/Calc/Directory.Packages.props index 442f4415d..4c99120d1 100644 --- a/CSharpBible/Calc/Directory.Packages.props +++ b/CSharpBible/Calc/Directory.Packages.props @@ -23,8 +23,8 @@ - - + + From 4a5096e2e5e5f7b813ad16bb33a55adc42d50723 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:04:03 +0200 Subject: [PATCH 67/96] Data --- .../Data/.github/copilot-instructions.md | 31 ++-- CSharpBible/Data/Data.sln | 155 +++++++++++++++++- 2 files changed, 172 insertions(+), 14 deletions(-) diff --git a/CSharpBible/Data/.github/copilot-instructions.md b/CSharpBible/Data/.github/copilot-instructions.md index e37d23d3c..77988b00d 100644 --- a/CSharpBible/Data/.github/copilot-instructions.md +++ b/CSharpBible/Data/.github/copilot-instructions.md @@ -5,9 +5,11 @@ Apply these defaults when working in this repository unless the user explicitly ## General Guidelines - Document code thoroughly in English. - Validate changes with relevant builds and tests before finishing. -- If requirements are unclear, ask clarifying questions before starting implementation. +- If requirements are unclear, ask clarifying questions before starting implementation or planning refinement. - Avoid UI text strings in core services. Use Enumerations instead, and keep UI-facing strings in the ViewModel/UI layer. - Prefer one class/interface/struct per file. +- document changes in an DevOps-manner markdown prefered, extrapolate bugs, tasks, baglogs and features +- Use `DevOps` as the planning directory in this workspace, and treat `.Info.md` as the general planning description file. Team terminology around Azure DevOps backlog items may differ from generic 'story' naming. ## Testing - Use `MSTest` in the latest practical version for new or updated tests. @@ -24,20 +26,23 @@ Apply these defaults when working in this repository unless the user explicitly - UI-facing strings and summary formatting should stay in the ViewModel/UI layer, not in extracted application logic services. ## Naming Conventions +- Distinguish between UI control naming and variable/field naming. - Use PascalCase for class names, method names, and properties. -- Use _camelCase for local/private variables and parameters. -- Use 1 letter prefixes for type of variable, e.g. in model classes, use: - - `s` for string, - - `i` for all int (8-128bit), - - `u` for Unsigned Int (8-128 bit), - - `x` for bool, - - `f` for float/double, -- Use 3 letter prefixes for UI elements, use: - - `lst` for list, +- Use `_camelCase` for private fields. +- Use `camelCase` for local variables and parameters. +- Use short 1-character prefixes for simple types only when they meaningfully disambiguate, e.g. + - `s` for `string` + - `i` for signed integer types + - `u` for unsigned integer types + - `x` for `bool` + - `f` for `float`, `double`, or `decimal` +- Prefer meaningful domain names over type prefixes when the intent is already clear. +- In UI code, use short 3-character prefixes for actual controls in views and code-behind, e.g. + - `lst` for list controls - `btn` for all kind of buttons, - - `edt` for all kind of text-inputs, - - `chk` for checkboxes, - - `lbl` for all kind of text-displaying elements, + - `edt` for any keyboard input control + - `lbl` for any text output control +- Do not use UI control prefixes for ViewModel properties or other non-UI members. ## Nullability - Use strict nullable reference types to indicate when a variable can be null, and handle nullability appropriately in code. diff --git a/CSharpBible/Data/Data.sln b/CSharpBible/Data/Data.sln index 283cdcc78..c6af4c85b 100644 --- a/CSharpBible/Data/Data.sln +++ b/CSharpBible/Data/Data.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 VisualStudioVersion = 18.0.11111.16 @@ -86,6 +86,61 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RepoMigrator.Tools.Pipeline EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RepoMigrator.App.State", "RepoMigrator\RepoMigrator.App.State\RepoMigrator.App.State.csproj", "{39D79860-9660-6BC9-FA71-859DCCFAA8DE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RepoMigrator.App.Logic", "RepoMigrator\RepoMigrator.App.Logic\RepoMigrator.App.Logic.csproj", "{784DEB57-3032-2F6E-D796-B9C40E2B1349}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DataAnalysis", "DataAnalysis", "{D5CAB17D-F57B-4423-B1CC-B5E4FFE9CBE1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TraceAnalysis", "TraceAnalysis", "{6BB16C4B-D416-4484-ACEF-1A47B631913E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataAnalysis.Core.Tests", "DataAnalysis\DataAnalysis.Core.Tests\DataAnalysis.Core.Tests.csproj", "{B8F7D539-BD4D-2CE5-D84C-E1ACF2AC562A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataAnalysis.WPF", "DataAnalysis\DataAnalysis.WPF\DataAnalysis.WPF.csproj", "{8AD9DDC7-BF8C-DEA5-00E4-DCB621D9E26C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataAnalysis.WPF.TestHarness", "DataAnalysis\DataAnalysis.WPF.TestHarness\DataAnalysis.WPF.TestHarness.csproj", "{21458ED8-7326-4CEB-E16E-23CF18752D92}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataConvert.Console", "DataAnalysis\DataConvert.Console\DataConvert.Console.csproj", "{1E6E7853-EC14-1EF7-4592-5A8C04EEEFEE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DevOps", "DevOps", "{BC5F1FA9-CC5A-4F9B-A765-E986E81F59B1}" + ProjectSection(SolutionItems) = preProject + DevOps\.info.md = DevOps\.info.md + DevOps\README.md = DevOps\README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Epics", "Epics", "{6A185ABC-B2A5-45C8-8402-1EE3A3E54102}" + ProjectSection(SolutionItems) = preProject + DevOps\Epics\830302-TraceAnalysis-and-AGV-reverse-simulation.md = DevOps\Epics\830302-TraceAnalysis-and-AGV-reverse-simulation.md + DevOps\Epics\README.md = DevOps\Epics\README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Features", "Features", "{E66EA522-9CAC-4E3D-988E-D55550FBA679}" + ProjectSection(SolutionItems) = preProject + DevOps\Features\830329-Filter-based-trace-intake-and-export-foundation.md = DevOps\Features\830329-Filter-based-trace-intake-and-export-foundation.md + DevOps\Features\README.md = DevOps\Features\README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Backlog-Items", "Backlog-Items", "{69FB2434-DA6E-4623-95FD-103AE1C4E8B4}" + ProjectSection(SolutionItems) = preProject + DevOps\BacklogItems\BI-830302-010-Define-canonical-trace-exchange-model.md = DevOps\BacklogItems\BI-830302-010-Define-canonical-trace-exchange-model.md + DevOps\BacklogItems\BI-830302-011-Create-pluggable-input-filters-for-initial-source-formats.md = DevOps\BacklogItems\BI-830302-011-Create-pluggable-input-filters-for-initial-source-formats.md + DevOps\BacklogItems\BI-830302-012-Create-CSV-output-filter.md = DevOps\BacklogItems\BI-830302-012-Create-CSV-output-filter.md + DevOps\BacklogItems\BI-830302-013-Create-Excel-output-filter.md = DevOps\BacklogItems\BI-830302-013-Create-Excel-output-filter.md + DevOps\BacklogItems\README.md = DevOps\BacklogItems\README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tasks", "Tasks", "{08C40D64-DCC0-436D-A857-4EF3EA655486}" + ProjectSection(SolutionItems) = preProject + DevOps\Tasks\README.md = DevOps\Tasks\README.md + DevOps\Tasks\T-830302-001-Identify-first-supported-source-formats.md = DevOps\Tasks\T-830302-001-Identify-first-supported-source-formats.md + DevOps\Tasks\T-830302-002-Define-canonical-fields-and-optional-metadata.md = DevOps\Tasks\T-830302-002-Define-canonical-fields-and-optional-metadata.md + DevOps\Tasks\T-830302-003-Specify-input-filter-interface-and-selection-strategy.md = DevOps\Tasks\T-830302-003-Specify-input-filter-interface-and-selection-strategy.md + DevOps\Tasks\T-830302-004-Specify-CSV-column-mapping-and-export-behavior.md = DevOps\Tasks\T-830302-004-Specify-CSV-column-mapping-and-export-behavior.md + DevOps\Tasks\T-830302-005-Specify-Excel-workbook-layout-and-export-behavior.md = DevOps\Tasks\T-830302-005-Specify-Excel-workbook-layout-and-export-behavior.md + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TraceAnalysis.Base", "TraceAnalysis\TraceAnalysis.Base\TraceAnalysis.Base.csproj", "{65231580-7BC5-4956-B1BC-D62133F5D3B1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TraceAnalysis.Filter.CSV", "TraceAnalysis\TraceAnalysis.Filter.CSV\TraceAnalysis.Filter.CSV.csproj", "{5FD84A6F-B42B-4269-92D0-F13A5CF24548}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -444,6 +499,90 @@ Global {39D79860-9660-6BC9-FA71-859DCCFAA8DE}.Release|x64.Build.0 = Release|Any CPU {39D79860-9660-6BC9-FA71-859DCCFAA8DE}.Release|x86.ActiveCfg = Release|Any CPU {39D79860-9660-6BC9-FA71-859DCCFAA8DE}.Release|x86.Build.0 = Release|Any CPU + {784DEB57-3032-2F6E-D796-B9C40E2B1349}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {784DEB57-3032-2F6E-D796-B9C40E2B1349}.Debug|Any CPU.Build.0 = Debug|Any CPU + {784DEB57-3032-2F6E-D796-B9C40E2B1349}.Debug|x64.ActiveCfg = Debug|Any CPU + {784DEB57-3032-2F6E-D796-B9C40E2B1349}.Debug|x64.Build.0 = Debug|Any CPU + {784DEB57-3032-2F6E-D796-B9C40E2B1349}.Debug|x86.ActiveCfg = Debug|Any CPU + {784DEB57-3032-2F6E-D796-B9C40E2B1349}.Debug|x86.Build.0 = Debug|Any CPU + {784DEB57-3032-2F6E-D796-B9C40E2B1349}.Release|Any CPU.ActiveCfg = Release|Any CPU + {784DEB57-3032-2F6E-D796-B9C40E2B1349}.Release|Any CPU.Build.0 = Release|Any CPU + {784DEB57-3032-2F6E-D796-B9C40E2B1349}.Release|x64.ActiveCfg = Release|Any CPU + {784DEB57-3032-2F6E-D796-B9C40E2B1349}.Release|x64.Build.0 = Release|Any CPU + {784DEB57-3032-2F6E-D796-B9C40E2B1349}.Release|x86.ActiveCfg = Release|Any CPU + {784DEB57-3032-2F6E-D796-B9C40E2B1349}.Release|x86.Build.0 = Release|Any CPU + {B8F7D539-BD4D-2CE5-D84C-E1ACF2AC562A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8F7D539-BD4D-2CE5-D84C-E1ACF2AC562A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8F7D539-BD4D-2CE5-D84C-E1ACF2AC562A}.Debug|x64.ActiveCfg = Debug|Any CPU + {B8F7D539-BD4D-2CE5-D84C-E1ACF2AC562A}.Debug|x64.Build.0 = Debug|Any CPU + {B8F7D539-BD4D-2CE5-D84C-E1ACF2AC562A}.Debug|x86.ActiveCfg = Debug|Any CPU + {B8F7D539-BD4D-2CE5-D84C-E1ACF2AC562A}.Debug|x86.Build.0 = Debug|Any CPU + {B8F7D539-BD4D-2CE5-D84C-E1ACF2AC562A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8F7D539-BD4D-2CE5-D84C-E1ACF2AC562A}.Release|Any CPU.Build.0 = Release|Any CPU + {B8F7D539-BD4D-2CE5-D84C-E1ACF2AC562A}.Release|x64.ActiveCfg = Release|Any CPU + {B8F7D539-BD4D-2CE5-D84C-E1ACF2AC562A}.Release|x64.Build.0 = Release|Any CPU + {B8F7D539-BD4D-2CE5-D84C-E1ACF2AC562A}.Release|x86.ActiveCfg = Release|Any CPU + {B8F7D539-BD4D-2CE5-D84C-E1ACF2AC562A}.Release|x86.Build.0 = Release|Any CPU + {8AD9DDC7-BF8C-DEA5-00E4-DCB621D9E26C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8AD9DDC7-BF8C-DEA5-00E4-DCB621D9E26C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8AD9DDC7-BF8C-DEA5-00E4-DCB621D9E26C}.Debug|x64.ActiveCfg = Debug|Any CPU + {8AD9DDC7-BF8C-DEA5-00E4-DCB621D9E26C}.Debug|x64.Build.0 = Debug|Any CPU + {8AD9DDC7-BF8C-DEA5-00E4-DCB621D9E26C}.Debug|x86.ActiveCfg = Debug|Any CPU + {8AD9DDC7-BF8C-DEA5-00E4-DCB621D9E26C}.Debug|x86.Build.0 = Debug|Any CPU + {8AD9DDC7-BF8C-DEA5-00E4-DCB621D9E26C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8AD9DDC7-BF8C-DEA5-00E4-DCB621D9E26C}.Release|Any CPU.Build.0 = Release|Any CPU + {8AD9DDC7-BF8C-DEA5-00E4-DCB621D9E26C}.Release|x64.ActiveCfg = Release|Any CPU + {8AD9DDC7-BF8C-DEA5-00E4-DCB621D9E26C}.Release|x64.Build.0 = Release|Any CPU + {8AD9DDC7-BF8C-DEA5-00E4-DCB621D9E26C}.Release|x86.ActiveCfg = Release|Any CPU + {8AD9DDC7-BF8C-DEA5-00E4-DCB621D9E26C}.Release|x86.Build.0 = Release|Any CPU + {21458ED8-7326-4CEB-E16E-23CF18752D92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {21458ED8-7326-4CEB-E16E-23CF18752D92}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21458ED8-7326-4CEB-E16E-23CF18752D92}.Debug|x64.ActiveCfg = Debug|Any CPU + {21458ED8-7326-4CEB-E16E-23CF18752D92}.Debug|x64.Build.0 = Debug|Any CPU + {21458ED8-7326-4CEB-E16E-23CF18752D92}.Debug|x86.ActiveCfg = Debug|Any CPU + {21458ED8-7326-4CEB-E16E-23CF18752D92}.Debug|x86.Build.0 = Debug|Any CPU + {21458ED8-7326-4CEB-E16E-23CF18752D92}.Release|Any CPU.ActiveCfg = Release|Any CPU + {21458ED8-7326-4CEB-E16E-23CF18752D92}.Release|Any CPU.Build.0 = Release|Any CPU + {21458ED8-7326-4CEB-E16E-23CF18752D92}.Release|x64.ActiveCfg = Release|Any CPU + {21458ED8-7326-4CEB-E16E-23CF18752D92}.Release|x64.Build.0 = Release|Any CPU + {21458ED8-7326-4CEB-E16E-23CF18752D92}.Release|x86.ActiveCfg = Release|Any CPU + {21458ED8-7326-4CEB-E16E-23CF18752D92}.Release|x86.Build.0 = Release|Any CPU + {1E6E7853-EC14-1EF7-4592-5A8C04EEEFEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1E6E7853-EC14-1EF7-4592-5A8C04EEEFEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1E6E7853-EC14-1EF7-4592-5A8C04EEEFEE}.Debug|x64.ActiveCfg = Debug|Any CPU + {1E6E7853-EC14-1EF7-4592-5A8C04EEEFEE}.Debug|x64.Build.0 = Debug|Any CPU + {1E6E7853-EC14-1EF7-4592-5A8C04EEEFEE}.Debug|x86.ActiveCfg = Debug|Any CPU + {1E6E7853-EC14-1EF7-4592-5A8C04EEEFEE}.Debug|x86.Build.0 = Debug|Any CPU + {1E6E7853-EC14-1EF7-4592-5A8C04EEEFEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1E6E7853-EC14-1EF7-4592-5A8C04EEEFEE}.Release|Any CPU.Build.0 = Release|Any CPU + {1E6E7853-EC14-1EF7-4592-5A8C04EEEFEE}.Release|x64.ActiveCfg = Release|Any CPU + {1E6E7853-EC14-1EF7-4592-5A8C04EEEFEE}.Release|x64.Build.0 = Release|Any CPU + {1E6E7853-EC14-1EF7-4592-5A8C04EEEFEE}.Release|x86.ActiveCfg = Release|Any CPU + {1E6E7853-EC14-1EF7-4592-5A8C04EEEFEE}.Release|x86.Build.0 = Release|Any CPU + {65231580-7BC5-4956-B1BC-D62133F5D3B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {65231580-7BC5-4956-B1BC-D62133F5D3B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {65231580-7BC5-4956-B1BC-D62133F5D3B1}.Debug|x64.ActiveCfg = Debug|Any CPU + {65231580-7BC5-4956-B1BC-D62133F5D3B1}.Debug|x64.Build.0 = Debug|Any CPU + {65231580-7BC5-4956-B1BC-D62133F5D3B1}.Debug|x86.ActiveCfg = Debug|Any CPU + {65231580-7BC5-4956-B1BC-D62133F5D3B1}.Debug|x86.Build.0 = Debug|Any CPU + {65231580-7BC5-4956-B1BC-D62133F5D3B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {65231580-7BC5-4956-B1BC-D62133F5D3B1}.Release|Any CPU.Build.0 = Release|Any CPU + {65231580-7BC5-4956-B1BC-D62133F5D3B1}.Release|x64.ActiveCfg = Release|Any CPU + {65231580-7BC5-4956-B1BC-D62133F5D3B1}.Release|x64.Build.0 = Release|Any CPU + {65231580-7BC5-4956-B1BC-D62133F5D3B1}.Release|x86.ActiveCfg = Release|Any CPU + {65231580-7BC5-4956-B1BC-D62133F5D3B1}.Release|x86.Build.0 = Release|Any CPU + {5FD84A6F-B42B-4269-92D0-F13A5CF24548}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5FD84A6F-B42B-4269-92D0-F13A5CF24548}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5FD84A6F-B42B-4269-92D0-F13A5CF24548}.Debug|x64.ActiveCfg = Debug|Any CPU + {5FD84A6F-B42B-4269-92D0-F13A5CF24548}.Debug|x64.Build.0 = Debug|Any CPU + {5FD84A6F-B42B-4269-92D0-F13A5CF24548}.Debug|x86.ActiveCfg = Debug|Any CPU + {5FD84A6F-B42B-4269-92D0-F13A5CF24548}.Debug|x86.Build.0 = Debug|Any CPU + {5FD84A6F-B42B-4269-92D0-F13A5CF24548}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5FD84A6F-B42B-4269-92D0-F13A5CF24548}.Release|Any CPU.Build.0 = Release|Any CPU + {5FD84A6F-B42B-4269-92D0-F13A5CF24548}.Release|x64.ActiveCfg = Release|Any CPU + {5FD84A6F-B42B-4269-92D0-F13A5CF24548}.Release|x64.Build.0 = Release|Any CPU + {5FD84A6F-B42B-4269-92D0-F13A5CF24548}.Release|x86.ActiveCfg = Release|Any CPU + {5FD84A6F-B42B-4269-92D0-F13A5CF24548}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -466,7 +605,9 @@ Global {9C6E06EC-4C60-4B02-AB2E-E24ACB81AA65} = {C72F7201-74F9-66F9-5C2C-ABF0F08448DC} {7446B1C7-C30B-41F4-AD7B-C21B6A805078} = {112ACC04-4DC7-4E32-80A4-508E79A28E0C} {62DE7992-D20A-43E3-B113-B299AD9D0092} = {112ACC04-4DC7-4E32-80A4-508E79A28E0C} + {A17E8BCA-5068-4FAF-B155-BACBAF5368DC} = {6BB16C4B-D416-4484-ACEF-1A47B631913E} {8CA6365C-0665-4D6F-8CC9-0A1339886316} = {51F6C20B-003C-430C-BED7-2A89F834E4C0} + {30EA08E2-CB3E-4049-B3C0-A77F1CBC7A3C} = {6BB16C4B-D416-4484-ACEF-1A47B631913E} {2780B100-CAB5-4C83-B158-ED7C737944B0} = {3C73C616-12F2-478C-9CAC-823780861BCD} {42156DBB-5FC0-3FE1-FC43-55400E7FDFAD} = {3C73C616-12F2-478C-9CAC-823780861BCD} {BFCB4F4A-F6A3-EB13-DB02-B0C1979AFDEA} = {3C73C616-12F2-478C-9CAC-823780861BCD} @@ -476,6 +617,18 @@ Global {521E30F6-FCC5-AB89-413C-475AFF2A6C2D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {E526046E-41C0-8852-26F4-3224217E43E6} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {39D79860-9660-6BC9-FA71-859DCCFAA8DE} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {784DEB57-3032-2F6E-D796-B9C40E2B1349} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {B8F7D539-BD4D-2CE5-D84C-E1ACF2AC562A} = {D5CAB17D-F57B-4423-B1CC-B5E4FFE9CBE1} + {8AD9DDC7-BF8C-DEA5-00E4-DCB621D9E26C} = {D5CAB17D-F57B-4423-B1CC-B5E4FFE9CBE1} + {21458ED8-7326-4CEB-E16E-23CF18752D92} = {D5CAB17D-F57B-4423-B1CC-B5E4FFE9CBE1} + {1E6E7853-EC14-1EF7-4592-5A8C04EEEFEE} = {D5CAB17D-F57B-4423-B1CC-B5E4FFE9CBE1} + {BC5F1FA9-CC5A-4F9B-A765-E986E81F59B1} = {6BB16C4B-D416-4484-ACEF-1A47B631913E} + {6A185ABC-B2A5-45C8-8402-1EE3A3E54102} = {BC5F1FA9-CC5A-4F9B-A765-E986E81F59B1} + {E66EA522-9CAC-4E3D-988E-D55550FBA679} = {BC5F1FA9-CC5A-4F9B-A765-E986E81F59B1} + {69FB2434-DA6E-4623-95FD-103AE1C4E8B4} = {BC5F1FA9-CC5A-4F9B-A765-E986E81F59B1} + {08C40D64-DCC0-436D-A857-4EF3EA655486} = {BC5F1FA9-CC5A-4F9B-A765-E986E81F59B1} + {65231580-7BC5-4956-B1BC-D62133F5D3B1} = {6BB16C4B-D416-4484-ACEF-1A47B631913E} + {5FD84A6F-B42B-4269-92D0-F13A5CF24548} = {6BB16C4B-D416-4484-ACEF-1A47B631913E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {128BE64A-28F5-47C5-A045-2352EF09BFBB} From 60d515c6681ad65a75f552615cdcd5c8c4638b1b Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:04:04 +0200 Subject: [PATCH 68/96] Games --- CSharpBible/Games/Directory.Packages.props | 2 +- CSharpBible/Games/Packages.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CSharpBible/Games/Directory.Packages.props b/CSharpBible/Games/Directory.Packages.props index 07fa7bc83..b2f483ad4 100644 --- a/CSharpBible/Games/Directory.Packages.props +++ b/CSharpBible/Games/Directory.Packages.props @@ -5,7 +5,7 @@ - + \ No newline at end of file diff --git a/CSharpBible/Games/Packages.props b/CSharpBible/Games/Packages.props index 83fe7d6ee..3c8d6d990 100644 --- a/CSharpBible/Games/Packages.props +++ b/CSharpBible/Games/Packages.props @@ -36,7 +36,7 @@ - + From 262c47468637affc5f13bd57aa82c2e39363cc26 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:04:04 +0200 Subject: [PATCH 69/96] Graphics --- CSharpBible/Graphics/.github/copilot-instructions.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CSharpBible/Graphics/.github/copilot-instructions.md b/CSharpBible/Graphics/.github/copilot-instructions.md index e4495db73..77988b00d 100644 --- a/CSharpBible/Graphics/.github/copilot-instructions.md +++ b/CSharpBible/Graphics/.github/copilot-instructions.md @@ -5,7 +5,11 @@ Apply these defaults when working in this repository unless the user explicitly ## General Guidelines - Document code thoroughly in English. - Validate changes with relevant builds and tests before finishing. -- If requirements are unclear, ask clarifying questions before starting implementation. +- If requirements are unclear, ask clarifying questions before starting implementation or planning refinement. +- Avoid UI text strings in core services. Use Enumerations instead, and keep UI-facing strings in the ViewModel/UI layer. +- Prefer one class/interface/struct per file. +- document changes in an DevOps-manner markdown prefered, extrapolate bugs, tasks, baglogs and features +- Use `DevOps` as the planning directory in this workspace, and treat `.Info.md` as the general planning description file. Team terminology around Azure DevOps backlog items may differ from generic 'story' naming. ## Testing - Use `MSTest` in the latest practical version for new or updated tests. @@ -17,7 +21,9 @@ Apply these defaults when working in this repository unless the user explicitly ## Architecture - Use MVVM architecture for UI components to separate concerns and improve testability, using CommunityToolkit.Mvvm for MVVM implementation. +- Prefer `NotifyPropertyChangedFor` over manual `OnPropertyChanged(nameof(...))` in CommunityToolkit.Mvvm observable properties where applicable. - Use Dependency Injection to manage dependencies and improve testability, using Microsoft.Extensions.DependencyInjection. +- UI-facing strings and summary formatting should stay in the ViewModel/UI layer, not in extracted application logic services. ## Naming Conventions - Distinguish between UI control naming and variable/field naming. @@ -33,7 +39,7 @@ Apply these defaults when working in this repository unless the user explicitly - Prefer meaningful domain names over type prefixes when the intent is already clear. - In UI code, use short 3-character prefixes for actual controls in views and code-behind, e.g. - `lst` for list controls - - `btn` for buttons + - `btn` for all kind of buttons, - `edt` for any keyboard input control - `lbl` for any text output control - Do not use UI control prefixes for ViewModel properties or other non-UI members. From 9517578c4b9d85346007b8548fe9fbe584e31f06 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:06:48 +0200 Subject: [PATCH 70/96] RnzTrauer.Console --- .../RnzTrauer/RnzTrauer.Console/Program.cs | 2 +- .../ViewModels/RnzTrauerConsoleViewModel.cs | 137 ++++++++++++------ 2 files changed, 91 insertions(+), 48 deletions(-) diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Console/Program.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Console/Program.cs index c1471efa9..a50930c43 100644 --- a/WinAhnenNew/RnzTrauer/RnzTrauer.Console/Program.cs +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Console/Program.cs @@ -18,7 +18,7 @@ { var xConfig = new RnzConfig(xServices.GetRequiredService()).Load(Path.Combine(AppContext.BaseDirectory, "RNZ_Config.json")); var xViewModel = xServices.GetRequiredService(); - xViewModel.Run(xConfig); + xViewModel.Run(xConfig, args.FirstOrDefault() ?? ""); } catch (FileNotFoundException ex) { diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Console/ViewModels/RnzTrauerConsoleViewModel.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Console/ViewModels/RnzTrauerConsoleViewModel.cs index 2513bec30..71906b78d 100644 --- a/WinAhnenNew/RnzTrauer/RnzTrauer.Console/ViewModels/RnzTrauerConsoleViewModel.cs +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Console/ViewModels/RnzTrauerConsoleViewModel.cs @@ -15,6 +15,44 @@ public sealed class RnzTrauerConsoleViewModel private readonly IHttpClientProxy _xHttpClient; private readonly IWebDriverFactory _xWebDriverFactory; + // Dictionary keys + private const string KeyContent = "content"; + private const string KeyHref = "href"; + private const string KeyLocalPath = "localpath"; + private const string KeyParent = "parent"; + private const string KeyPdfText = "pdfText"; + private const string KeyUrl = "url"; + + // File extensions + private const string ExtHtml = ".html"; + private const string ExtJpeg = ".jpeg"; + private const string ExtJson = ".json"; + private const string ExtPdf = ".pdf"; + private const string ExtPng = ".png"; + + // Binary file signatures + private const string SignatureHtmlDoctype = " /// Initializes a new instance of the class. /// @@ -29,9 +67,9 @@ public RnzTrauerConsoleViewModel(ConsoleOutputView xView, IFile xFile, IHttpClie /// /// Runs the RNZ scraping and import workflow. /// - public void Run(RnzConfig xConfig) + public void Run(RnzConfig xConfig, string sParam1 = "") { - _view.WriteLine("Start..."); + _view.WriteLine(UiMsgStart); var xProgress = new Progress(xUpdate => { if (xUpdate.WriteLine) @@ -46,17 +84,22 @@ public void Run(RnzConfig xConfig) using var xWebHandler = new WebHandler(xConfig, _xHttpClient, _xWebDriverFactory, xProgress); xWebHandler.InitPage(); - _view.WriteLine("Init..."); - var iOffset = 35; + _view.WriteLine(UiMsgInit); + var sBaseHost = Uri.TryCreate(xConfig.Url, UriKind.Absolute, out var xBaseUri) + ? xBaseUri.GetLeftPart(UriPartial.Authority) + : string.Empty; + var sSearchBaseUrl = $"{sBaseHost}{RnzSearchPath}"; + DateTime today = DateTime.Today; + var iOffset = DateTime.TryParse(sParam1,out var dtStart)?(today- dtStart).Days : 0; var iDayDelta = 0; - while (iDayDelta <= 800) + while (iDayDelta <= 14) { - var dtCurrent = DateOnly.FromDateTime(DateTime.Today).AddDays(-(iDayDelta + iOffset)); + var dtCurrent = DateOnly.FromDateTime(today).AddDays(-(iDayDelta + iOffset)); iDayDelta += 1; - var sStart = $"https://trauer.rnz.de/traueranzeigen-suche/erscheinungstag-{dtCurrent.Day:00}-{dtCurrent.Month:00}-{dtCurrent.Year:0000}"; + var sStart = $"{sSearchBaseUrl}{dtCurrent.Day:00}-{dtCurrent.Month:00}-{dtCurrent.Year:0000}"; var (dPages, arrItems) = xWebHandler.GetData1(sStart); - _view.Write("Compute "); + _view.Write(UiMsgCompute); for (var iIndex = 0; iIndex < arrItems.Count; iIndex++) { var dEntry = new Dictionary(arrItems[iIndex], StringComparer.Ordinal); @@ -65,18 +108,18 @@ public void Run(RnzConfig xConfig) if (dEntry.TryGetValue(WebHandler.CsData, out var xDataObject) && xDataObject is byte[] arrData && arrData.Length >= 10) { var sPrefix = Encoding.ASCII.GetString(arrData.Take(10).ToArray()); - if (sPrefix.Contains("PDF", StringComparison.Ordinal)) + if (sPrefix.Contains(SignaturePdf, StringComparison.Ordinal)) { - dEntry["pdfText"] = PortedHelpers.PdfText(arrData); + dEntry[KeyPdfText] = PortedHelpers.PdfText(arrData); arrItems[iIndex] = dEntry; - if (dEntry.TryGetValue("parent", out var xParentObject)) + if (dEntry.TryGetValue(KeyParent, out var xParentObject)) { var sParent = Convert.ToString(xParentObject) ?? string.Empty; if (dPages.TryGetValue(sParent, out var dParentPage) && dEntry.TryGetValue(WebHandler.CsSrc, out var xSourceObject)) { dParentPage[Convert.ToString(xSourceObject) ?? string.Empty] = new Dictionary { - ["pdfText"] = dEntry["pdfText"] + [KeyPdfText] = dEntry[KeyPdfText] }; } } @@ -94,9 +137,9 @@ public void Run(RnzConfig xConfig) } } - _view.Write("\nSave "); + _view.Write(UiMsgSave); SavePages(xConfig, dPages); - _view.Write("\nSave Media:"); + _view.Write(UiMsgSaveMedia); SaveMedia(xConfig, arrItems, dtCurrent); _view.WriteLine(); } @@ -104,13 +147,13 @@ public void Run(RnzConfig xConfig) xWebHandler.Close(); using var xDataHandler = new DataHandler(xConfig, _xFile); - iDayDelta = 0; - while (iDayDelta <= 14) + iDayDelta = -7; + while (iDayDelta <= 30) { var dtCurrent = DateOnly.FromDateTime(DateTime.Today).AddDays(-(iDayDelta + iOffset)); _view.WriteLine($"Handle: {dtCurrent}"); iDayDelta += 1; - foreach (var sAnnouncementType in new[] { "todesanzeigen", "nachrufe", "danksagungen", "_" }) + foreach (var sAnnouncementType in AnnouncementTypes) { _view.WriteLine($"Type: {sAnnouncementType}"); var iPage = 0; @@ -119,12 +162,12 @@ public void Run(RnzConfig xConfig) iPage += 1; _view.WriteLine($"Page: {iPage}"); var sStart = iPage == 1 - ? $"https://trauer.rnz.de/traueranzeigen-suche/erscheinungstag-{dtCurrent.Day:00}-{dtCurrent.Month:00}-{dtCurrent.Year:0000}/anzeigenart-{sAnnouncementType}" - : $"https://trauer.rnz.de/traueranzeigen-suche/erscheinungstag-{dtCurrent.Day:00}-{dtCurrent.Month:00}-{dtCurrent.Year:0000}/anzeigenart-{sAnnouncementType}/seite-{iPage}"; + ? $"{sSearchBaseUrl}{dtCurrent.Day:00}-{dtCurrent.Month:00}-{dtCurrent.Year:0000}{PathAnzeigenArt}{sAnnouncementType}" + : $"{sSearchBaseUrl}{dtCurrent.Day:00}-{dtCurrent.Month:00}-{dtCurrent.Year:0000}{PathAnzeigenArt}{sAnnouncementType}{PathSeite}{iPage}"; var sPath = PortedHelpers.GetLocalPath(sStart, xConfig.LocalPath, dtCurrent); var sJsonFile = Path.HasExtension(sPath) - ? Path.ChangeExtension(sPath, ".json") - : sPath + ".json"; + ? Path.ChangeExtension(sPath, ExtJson) + : sPath + ExtJson; if (_xFile.Exists(sJsonFile)) { var xData = JsonNode.Parse(_xFile.ReadAllText(sJsonFile)); @@ -153,13 +196,13 @@ private void SavePages(RnzConfig xConfig, Dictionary(StringComparer.Ordinal); var xParentChanged = false; - if (dPage.TryGetValue("parent", out var xParentObject) && xParentObject is List arrParents) + if (dPage.TryGetValue(KeyParent, out var xParentObject) && xParentObject is List arrParents) { foreach (var xParentValue in arrParents) { var sParent = Convert.ToString(xParentValue) ?? string.Empty; var sParentPath = PortedHelpers.GetLocalPath(sParent, xConfig.LocalPath); - sParentPath = Path.HasExtension(sParentPath) ? Path.ChangeExtension(sParentPath, ".json") : sParentPath + ".json"; + sParentPath = Path.HasExtension(sParentPath) ? Path.ChangeExtension(sParentPath, ExtJson) : sParentPath + ExtJson; if (_xFile.Exists(sParentPath)) { try @@ -182,12 +225,12 @@ private void SavePages(RnzConfig xConfig, Dictionary arrParentList) + if (dPage.TryGetValue(KeyParent, out xParentObject) && xParentObject is List arrParentList) { foreach (var xParentValue in arrParentList) { var sParent = Convert.ToString(xParentValue) ?? string.Empty; var dEntryCopy = new Dictionary(dPage, StringComparer.Ordinal); - dEntryCopy.Remove("content"); - dEntryCopy["localpath"] = sJsonPath; - var sUrl = Convert.ToString(dPage["url"]) ?? string.Empty; + dEntryCopy.Remove(KeyContent); + dEntryCopy[KeyLocalPath] = sJsonPath; + var sUrl = Convert.ToString(dPage[KeyUrl]) ?? string.Empty; if (!dParentData[sParent].ContainsKey(sUrl)) { dParentData[sParent][sUrl] = PortedHelpers.ToJsonObject(dEntryCopy); @@ -257,9 +300,9 @@ private void SaveMedia(RnzConfig xConfig, List> arrI { var dEntryCopy = new Dictionary(dEntry, StringComparer.Ordinal); dEntryCopy.Remove(WebHandler.CsData); - var sParent = Convert.ToString(dEntry.GetValueOrDefault("parent")) ?? string.Empty; + var sParent = Convert.ToString(dEntry.GetValueOrDefault(KeyParent)) ?? string.Empty; var sParentPath = PortedHelpers.GetLocalPath(sParent, xConfig.LocalPath); - sParentPath = Path.HasExtension(sParentPath) ? Path.ChangeExtension(sParentPath, ".json") : sParentPath + ".json"; + sParentPath = Path.HasExtension(sParentPath) ? Path.ChangeExtension(sParentPath, ExtJson) : sParentPath + ExtJson; JsonObject xParentData; var xParentChanged = false; if (_xFile.Exists(sParentPath)) @@ -280,16 +323,16 @@ private void SaveMedia(RnzConfig xConfig, List> arrI xParentChanged = true; } - if (dEntryCopy.TryGetValue("Header", out var xHeaderObject) && xHeaderObject is Dictionary dHeaders) + if (dEntryCopy.TryGetValue(WebHandler.CsHeader, out var xHeaderObject) && xHeaderObject is Dictionary dHeaders) { - dEntryCopy["Header"] = dHeaders.ToDictionary(k => k.Key, v => (object?)v.Value, StringComparer.OrdinalIgnoreCase); + dEntryCopy[WebHandler.CsHeader] = dHeaders.ToDictionary(k => k.Key, v => (object?)v.Value, StringComparer.OrdinalIgnoreCase); } - if (dEntry.TryGetValue("href", out var xHrefObject)) + if (dEntry.TryGetValue(KeyHref, out var xHrefObject)) { var sHref = Convert.ToString(xHrefObject) ?? string.Empty; var sLocalPath = PortedHelpers.GetLocalPath(sHref, xConfig.LocalPath, dtCurrent); - var sDataPath = Path.Combine(sLocalPath, $"data_{dtCurrent:yyyy-MM-dd}.json"); + var sDataPath = Path.Combine(sLocalPath, $"{DataFilePrefix}{dtCurrent.ToString(DateFormatDaily)}{ExtJson}"); Directory.CreateDirectory(Path.GetDirectoryName(sDataPath)!); _xFile.WriteAllText(sDataPath, PortedHelpers.ToJsonObject(dEntryCopy).ToJsonString(PortedHelpers.JsonOptions)); } @@ -300,26 +343,26 @@ private void SaveMedia(RnzConfig xConfig, List> arrI var sLocalPath = PortedHelpers.GetLocalPath(sSource, xConfig.LocalPath); var sFilePath = sLocalPath; var sPrefix = Encoding.ASCII.GetString(arrData.Take(10).ToArray()); - if (sPrefix.Contains("PNG", StringComparison.Ordinal)) + if (sPrefix.Contains(SignaturePng, StringComparison.Ordinal)) { - sFilePath = Path.ChangeExtension(sFilePath, ".png"); + sFilePath = Path.ChangeExtension(sFilePath, ExtPng); } - else if (sPrefix.Contains("PDF", StringComparison.Ordinal)) + else if (sPrefix.Contains(SignaturePdf, StringComparison.Ordinal)) { - sFilePath = Path.ChangeExtension(sFilePath, ".pdf"); + sFilePath = Path.ChangeExtension(sFilePath, ExtPdf); } - else if (sPrefix.Contains("JFIF", StringComparison.Ordinal)) + else if (sPrefix.Contains(SignatureJfif, StringComparison.Ordinal)) { - sFilePath = Path.ChangeExtension(sFilePath, ".jpeg"); + sFilePath = Path.ChangeExtension(sFilePath, ExtJpeg); } - else if (sPrefix.Contains(" Date: Sun, 12 Apr 2026 19:06:48 +0200 Subject: [PATCH 71/96] RnzTrauer.Core --- .../RnzTrauer.Core/Services/DataHandler.cs | 96 ++-- .../RnzTrauer.Core/Services/WebHandler.cs | 534 +++++++++++++----- 2 files changed, 469 insertions(+), 161 deletions(-) diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/DataHandler.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/DataHandler.cs index d2814125f..9a2829b56 100644 --- a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/DataHandler.cs +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/DataHandler.cs @@ -27,7 +27,8 @@ public DataHandler(DatabaseSettings xSettings, IFile xFile) UserID = xSettings.DBuser, Password = xSettings.DBpass, Database = xSettings.DB, - AllowUserVariables = true + AllowUserVariables = true, + ConvertZeroDateTime = true }.ConnectionString; _dbConn = new MySqlConnection(sConnectionString); @@ -264,7 +265,7 @@ public bool UpdateTrauerAnz(List> arrNewValues, List /// public List> TrauerFallByUrl(string sUrl) { - return Query("SELECT * FROM Trauerfall WHERE url=@url", xCommand => xCommand.Parameters.AddWithValue("@url", sUrl)); + return Query("SELECT idTrauerfall, url FROM Trauerfall WHERE url=@url", xCommand => xCommand.Parameters.AddWithValue("@url", sUrl)); } /// @@ -309,7 +310,16 @@ public long AppendTrauerFall(Dictionary dTrauerfall) /// public long AppendTrauerAnz(long iTrauerfallId, Dictionary dTrauerfall, string sLocalPath) { - var sPath = Directory.GetParent(dTrauerfall.Cond("img"))?.FullName ?? string.Empty; + string sImgPath = dTrauerfall.Cond("img"); + if (string.IsNullOrEmpty(sImgPath)) + { + sImgPath = dTrauerfall.Cond("pdf"); + } + if (string.IsNullOrEmpty(sImgPath)) + { + sImgPath = sLocalPath; + } + var sPath = Directory.GetParent(sImgPath)?.FullName ?? string.Empty; var sNormalizedLocalPath = sPath.Replace(sLocalPath, string.Empty, StringComparison.Ordinal); var sProfileBase = Directory.GetParent(Directory.GetParent(sPath)?.FullName ?? string.Empty)?.FullName ?? string.Empty; var sProfileImage = dTrauerfall.Cond("profImg").Replace(sProfileBase, "..\\..", StringComparison.Ordinal); @@ -345,12 +355,21 @@ public long AppendTrauerAnz(long iTrauerfallId, Dictionary dTra /// public void SetTrauerAnz(Dictionary dCurrent, Dictionary dTrauerfall, string sLocalPath) { - var sPath = Directory.GetParent(PortedHelpers.Cond(dTrauerfall, "img"))?.FullName ?? string.Empty; + string sImgPath = dTrauerfall.Cond("img"); + if (string.IsNullOrEmpty(sImgPath)) + { + sImgPath = dTrauerfall.Cond("pdf"); + } + if (string.IsNullOrEmpty(sImgPath)) + { + sImgPath = sLocalPath; + } + var sPath = Directory.GetParent(sImgPath)?.FullName ?? string.Empty; var sNormalizedLocalPath = sPath.Replace(sLocalPath, string.Empty, StringComparison.Ordinal); var sProfileBase = Directory.GetParent(Directory.GetParent(sPath)?.FullName ?? string.Empty)?.FullName ?? string.Empty; - var sProfileImage = PortedHelpers.Cond(dTrauerfall, "profImg").Replace(sProfileBase, "..\\..", StringComparison.Ordinal); + var sProfileImage = dTrauerfall.Cond( "profImg").Replace(sProfileBase, "..\\..", StringComparison.Ordinal); var iRubrik = dCurrent.TryGetValue("Rubrik", out var xCurrentRubrik) && int.TryParse(Convert.ToString(xCurrentRubrik, CultureInfo.InvariantCulture), out var iParsed) ? iParsed : 8050; - var sFilter = PortedHelpers.Cond(dTrauerfall, "filter"); + var sFilter = dTrauerfall.Cond( "filter"); if (sFilter == "danksagungen") { iRubrik = 8060; @@ -378,23 +397,23 @@ public void SetTrauerAnz(Dictionary dCurrent, Dictionary { - ["url"] = PortedHelpers.Cond(dTrauerfall, "url"), - ["Announcement"] = int.TryParse(PortedHelpers.Cond(dTrauerfall, "id"), out var iId) ? iId : 0, - ["release"] = ToDbValue(PortedHelpers.Str2Date(PortedHelpers.Cond(dTrauerfall, "publish"))), + ["url"] = dTrauerfall.Cond( "url"), + ["Announcement"] = int.TryParse(dTrauerfall.Cond( "id"), out var iId) ? iId : 0, + ["release"] = ToDbValue(PortedHelpers.Str2Date(dTrauerfall.Cond( "publish"))), ["localpath"] = sNormalizedLocalPath, - ["pngFile"] = Path.GetFileName(PortedHelpers.Cond(dTrauerfall, "img")), - ["pdfFile"] = Path.GetFileName(PortedHelpers.Cond(dTrauerfall, "pdf")), + ["pngFile"] = Path.GetFileName(dTrauerfall.Cond( "img")), + ["pdfFile"] = Path.GetFileName(dTrauerfall.Cond( "pdf")), ["Additional"] = JsonSerializer.Serialize(dTrauerfall, PortedHelpers.JsonOptions), ["Firstname"] = sFirstName, ["Lastname"] = sLastName, - ["Birthname"] = PortedHelpers.Cond(dTrauerfall, "Birthname"), - ["Birth"] = ToDbValue(PortedHelpers.Str2Date(TrimLeadingTwo(PortedHelpers.Cond(dTrauerfall, "Birth")))), - ["Death"] = ToDbValue(PortedHelpers.Str2Date(TrimLeadingTwo(PortedHelpers.Cond(dTrauerfall, "Death")))), - ["Place"] = PortedHelpers.Cond(dTrauerfall, "Place"), - ["Info"] = PortedHelpers.Cond(dTrauerfall, "pdfText"), + ["Birthname"] = dTrauerfall.Cond( "Birthname"), + ["Birth"] = ToDbValue(PortedHelpers.Str2Date(TrimLeadingTwo(dTrauerfall.Cond( "Birth")))), + ["Death"] = ToDbValue(PortedHelpers.Str2Date(TrimLeadingTwo(dTrauerfall.Cond( "Death")))), + ["Place"] = dTrauerfall.Cond( "Place"), + ["Info"] = dTrauerfall.Cond( "pdfText"), ["ProfileImg"] = sProfileImage, ["Rubrik"] = iRubrik }) @@ -413,12 +432,12 @@ public long AppendLegacyTAnz(string sAuftrag, Dictionary dTraue "INSERT INTO `RNZ-Traueranzeigen`.`Anzeigen` (`Auftrag`, `url`, `Announcement`, `release`,`localpath`, `pngFile`, `pdfFile`, `Additional`) VALUES (@auftrag, @url, @announcement, @release, @localpath, @pngFile, @pdfFile, @additional);", _dbConn); xCommand.Parameters.AddWithValue("@auftrag", sAuftrag); - xCommand.Parameters.AddWithValue("@url", PortedHelpers.Cond(dTrauerfall, "url")); - xCommand.Parameters.AddWithValue("@announcement", int.TryParse(PortedHelpers.Cond(dTrauerfall, "id"), out var iId) ? iId : 0); - xCommand.Parameters.AddWithValue("@release", ToDbValue(PortedHelpers.Str2Date(PortedHelpers.Cond(dTrauerfall, "publish")))); + xCommand.Parameters.AddWithValue("@url", dTrauerfall.Cond( "url")); + xCommand.Parameters.AddWithValue("@announcement", int.TryParse(dTrauerfall.Cond( "id"), out var iId) ? iId : 0); + xCommand.Parameters.AddWithValue("@release", ToDbValue(PortedHelpers.Str2Date(dTrauerfall.Cond( "publish")))); xCommand.Parameters.AddWithValue("@localpath", sNormalizedLocalPath); - xCommand.Parameters.AddWithValue("@pngFile", Path.GetFileName(PortedHelpers.Cond(dTrauerfall, "img"))); - xCommand.Parameters.AddWithValue("@pdfFile", Path.GetFileName(PortedHelpers.Cond(dTrauerfall, "pdf"))); + xCommand.Parameters.AddWithValue("@pngFile", Path.GetFileName(dTrauerfall.Cond( "img"))); + xCommand.Parameters.AddWithValue("@pdfFile", Path.GetFileName(dTrauerfall.Cond( "pdf"))); xCommand.Parameters.AddWithValue("@additional", JsonSerializer.Serialize(dTrauerfall, PortedHelpers.JsonOptions)); xCommand.ExecuteNonQuery(); return xCommand.LastInsertedId; @@ -431,12 +450,20 @@ public void TrauerDataToDb(IEnumerable> arrData, str { foreach (var dAnnouncement in arrData) { - var arrCurrentCases = TrauerFallByUrl(PortedHelpers.Cond(dAnnouncement, "url")); + var dtCreated = PortedHelpers.Str2Date(dAnnouncement.Cond("created_on")); + var dtPublished = PortedHelpers.Str2Date(dAnnouncement.Cond("publish")); + if (!dtCreated.HasValue || !dtPublished.HasValue) + { + Console.WriteLine($"Skip incomplete announcement (missing created/publish): {dAnnouncement.Cond("url")}"); + continue; + } + + var arrCurrentCases = TrauerFallByUrl(dAnnouncement.Cond( "url")); var iTrauerfallId = arrCurrentCases.Count == 0 ? AppendTrauerFall(dAnnouncement) - : Convert.ToInt64(arrCurrentCases[0].Values.First(), CultureInfo.InvariantCulture); + : Convert.ToInt64(arrCurrentCases[0]["idTrauerfall"], CultureInfo.InvariantCulture); - var arrCurrentAnnouncements = TrauerAnz(int.TryParse(PortedHelpers.Cond(dAnnouncement, "id"), out var iId) ? iId : 0); + var arrCurrentAnnouncements = TrauerAnz(int.TryParse(dAnnouncement.Cond( "id"), out var iId) ? iId : 0); if (arrCurrentAnnouncements.Count == 0) { Console.Write('+'); @@ -514,13 +541,18 @@ private bool UpdateRows(string sTable, List> arrNewV var dRow = new Dictionary(StringComparer.Ordinal); for (var iIndex = 0; iIndex < xReader.FieldCount; iIndex++) { - dRow[xReader.GetName(iIndex)] = xReader.IsDBNull(iIndex) - ? null - : xReader.GetValue(iIndex) switch - { - DateTime dtValue => dtValue.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture), - _ => xReader.GetValue(iIndex) - }; + if (xReader.IsDBNull(iIndex)) + { + dRow[xReader.GetName(iIndex)] = null; + continue; + } + + var xValue = xReader.GetValue(iIndex); + dRow[xReader.GetName(iIndex)] = xValue switch + { + DateTime dtValue => dtValue.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture), + _ => xValue + }; } arrData.Add(dRow); diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/WebHandler.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/WebHandler.cs index 65af00302..78e4ce0be 100644 --- a/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/WebHandler.cs +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Core/Services/WebHandler.cs @@ -24,18 +24,110 @@ public sealed class WebHandler : IDisposable /// public const string CsSrc = "Src"; + private const string AnnouncementPrefix = "ANZ"; + private const string DataAttributeName = "data-original"; + private const string HtmlAttrAlt = "alt"; + private const string HtmlAttrClass = "class"; + private const string HtmlAttrHref = "href"; + private const string HtmlAttrId = "id"; + private const string HtmlAttrStyle = "style"; + private const string HtmlAttrTarget = "target"; + private const string HtmlAttrTitle = "title"; + private const string HtmlAttrSrc = "src"; + private const string HtmlClassDetailColumn = "col-sm-6"; + private const string HtmlClassContentColumn = "col-12"; + private const string HtmlClassWideColumn = "col-xl-8"; + private const string HtmlTagA = "a"; + private const string HtmlTagDiv = "div"; + private const string HtmlTagHeading1 = "h1"; + private const string HtmlTagImage = "img"; + private const string HtmlTagSection = "section"; + private const string HtmlTagBody = "body"; + private const string ImageFileJpg = ".jpg"; + private const string ImageFilePng = ".png"; + private const string ImageSignaturePng = "PNG"; + private const string KeyBirth = "Birth"; + private const string KeyContent = "content"; + private const string KeyCreatedBy = "created_by"; + private const string KeyCreatedOn = "created_on"; + private const string KeyDeath = "Death"; + private const string KeyFilter = "filter"; + private const string KeyHref = "href"; + private const string KeyId = "id"; + private const string KeyIdAnz = "id-anz"; + private const string KeyImages = "imgs"; + private const string KeyInfo = "Info"; + private const string KeyLinks = "links"; + private const string KeyMedia = "media"; + private const string KeyName = "name"; + private const string KeyParent = "parent"; + private const string KeyPlace = "Place"; + private const string KeySections = "sections"; + private const string KeyTag = "tag"; + private const string KeyText = "text"; + private const string KeyTitle = "Title"; + private const string KeyUrl = "url"; + private const string KeyVisits = "visits"; + private const string LoginEmailAddressId = "emailAddress"; + private const string LoginFormId = "form"; + private const string LoginPasswordId = "password"; + private const string MediaServerToken = "MEDIASERVER"; + private const string MissingDriverMessage = "The web driver has not been initialized."; + private const string NextLinkMarker = ">"; + private const string PageBlockItemClass = "c-blockitem"; + private const string PagePathAnzeigen = "/anzeigen"; + private const string PagePathAnzeigenArt = "/anzeigenart-"; + private const string PagePathSeparator = "/"; + private const string PageTitleRnz = "RNZ"; + private const string BirthMarker = "*"; + private const string CommaSeparator = ","; + private const string PlaceSeparator = "in"; + private const string SpaceSeparator = " "; + private const string ErrorMarker404 = "404"; + private const string ErrorMarkerNotFound = "not found"; + private const string ErrorMarkerServiceTimeout = "service timeout"; + private const string ErrorMarkerTimeout = "timeout"; + private const string ErrorMarkerServiceUnavailable = "service unavailable"; + private const string ErrorMarkerTemporarilyUnavailable = "temporarily unavailable"; + private const string ErrorMarkerBadGateway = "bad gateway"; + private const string ErrorMarkerGatewayTimeout = "gateway timeout"; + private const string ErrorMarker504GatewayTimeOut = "504 gateway time-out"; + private const string ErrorMarkerServerDidntRespondInTime = "the server didn't respond in time"; + private const string ErrorMarkerServerDidNotRespondInTime = "the server did not respond in time"; + private const string ErrorMarkerRequestTimeout = "request timeout"; + private const string ErrorMarkerConnectionTimeout = "connection timeout"; + private const string ErrorMarkerOperationTimedOut = "operation timed out"; + private const int MaxReloadAttempts = 6; + private const int NavigationWaitCycles = 20; + private const int NavigationWaitMilliseconds = 500; + + // UI strings from the RNZ portal. + private const string UiActionLargeView = "Großansicht"; + private const string UiActionSave = "Speichern"; + private const string UiLabelCreated = "Erstellt"; + private const string UiLabelCreatedOn = "Angelegt"; + private const string UiLabelVisits = "Besuche"; + private const string UiProgressDot = "."; + private const string UiProgressGetSubPages = "\nGet Subpages:"; + + private static readonly string[] AnnouncementTypes = ["nachrufe", "danksagungen", "todesanzeigen", "_"]; + private static readonly Dictionary _tagAttributes = new(StringComparer.OrdinalIgnoreCase) { - ["section"] = ["class", "id"], - ["div"] = ["class", "id"], - ["img"] = ["class", "title", "alt", "src", "style", "data-original"], - ["a"] = ["title", "target", "href"] + [HtmlTagSection] = [HtmlAttrClass, HtmlAttrId], + [HtmlTagDiv] = [HtmlAttrClass, HtmlAttrId], + [HtmlTagImage] = [HtmlAttrClass, HtmlAttrTitle, HtmlAttrAlt, CsSrc, HtmlAttrSrc, HtmlAttrStyle, DataAttributeName], + [HtmlTagA] = [HtmlAttrTitle, HtmlAttrTarget, HtmlAttrHref] }; private readonly RnzConfig _config; + private readonly string _sBaseHost; private readonly IHttpClientProxy _xHttpClient; private readonly IWebDriverFactory _xWebDriverFactory; private readonly IProgress? _xProgress; + private readonly HashSet _hsIndexedSubPages = new(StringComparer.Ordinal); + private readonly Dictionary _dIndexedBinaryData = new(StringComparer.Ordinal); + private readonly Dictionary> _dIndexedBinaryHeaders = new(StringComparer.Ordinal); /// /// Initializes a new instance of the class. @@ -43,6 +135,9 @@ public sealed class WebHandler : IDisposable public WebHandler(RnzConfig xConfig, IHttpClientProxy xHttpClient, IWebDriverFactory xWebDriverFactory, IProgress? xProgress = null) { _config = xConfig; + _sBaseHost = Uri.TryCreate(_config.Url, UriKind.Absolute, out var xUri) + ? xUri.GetLeftPart(UriPartial.Authority) + : string.Empty; _xHttpClient = xHttpClient ?? throw new ArgumentNullException(nameof(xHttpClient)); _xWebDriverFactory = xWebDriverFactory ?? throw new ArgumentNullException(nameof(xWebDriverFactory)); _xProgress = xProgress; @@ -68,9 +163,9 @@ public void InitPage() Driver = _xWebDriverFactory.Create(); Driver.Navigate().GoToUrl(_config.Url); - Driver.FindElement(By.Id("emailAddress")).SendKeys(_config.User); - Driver.FindElement(By.Id("password")).SendKeys(_config.Password); - Driver.FindElement(By.Id("form")).Submit(); + Driver.FindElement(By.Id(LoginEmailAddressId)).SendKeys(_config.User); + Driver.FindElement(By.Id(LoginPasswordId)).SendKeys(_config.Password); + Driver.FindElement(By.Id(LoginFormId)).Submit(); while (Driver.Title == _config.Title || string.IsNullOrEmpty(Driver.Title)) { Thread.Sleep(500); @@ -85,15 +180,35 @@ public void InitPage() public List> Wdr2List(ISearchContext xWebElement, WebQuery xQuery) { var arrResult = new List>(); - foreach (var xElement in xWebElement.FindElements(xQuery.By)) + IReadOnlyCollection arrElements; + try + { + arrElements = xWebElement.FindElements(xQuery.By); + } + catch (StaleElementReferenceException) + { + return arrResult; + } + + foreach (var xElement in arrElements) { + string sTagName; + try + { + sTagName = xElement.TagName; + } + catch (StaleElementReferenceException) + { + continue; + } + var dItem = new Dictionary { - ["tag"] = xElement.TagName + [KeyTag] = sTagName }; arrResult.Add(dItem); - if (_tagAttributes.TryGetValue(xElement.TagName, out var arrAttributes)) + if (_tagAttributes.TryGetValue(sTagName, out var arrAttributes)) { foreach (var sAttribute in arrAttributes) { @@ -110,18 +225,22 @@ public void InitPage() try { - dItem["text"] = (xElement.Text ?? string.Empty).Split(Environment.NewLine, StringSplitOptions.None).ToList(); + dItem[KeyText] = (xElement.Text ?? string.Empty).Split(Environment.NewLine, StringSplitOptions.None).ToList(); } catch { } - - foreach (var xChildQuery in xQuery.Children) + try + { + foreach (var xChildQuery in xQuery.Children) + { + dItem[xChildQuery.Name] = Wdr2List(xElement, xChildQuery); + } + } + catch { - dItem[xChildQuery.Name] = Wdr2List(xElement, xChildQuery); } } - return arrResult; } @@ -130,42 +249,67 @@ public void InitPage() /// public (Dictionary> Pages, List> Items) GetData1(string sStart, int iMaxPage = 30) { - var xDriver = Driver ?? throw new InvalidOperationException("The web driver has not been initialized."); + var xDriver = Driver ?? throw new InvalidOperationException(MissingDriverMessage); var dPages = new Dictionary>(StringComparer.Ordinal); var arrItems = new List>(); - foreach (var sAnnouncementType in new[] { "nachrufe", "danksagungen", "todesanzeigen", "_" }) + foreach (var sAnnouncementType in AnnouncementTypes) { - xDriver.Navigate().GoToUrl($"{sStart}/anzeigenart-{sAnnouncementType}"); - while (xDriver.Title == "RNZ" || string.IsNullOrEmpty(xDriver.Title)) + if (!NavigateWithReloadRetry($"{sStart}{PagePathAnzeigenArt}{sAnnouncementType}")) { - Thread.Sleep(500); + continue; } - var sNextUrl = "."; + var sNextUrl = UiProgressDot; var iCounter = 0; while (!string.IsNullOrEmpty(sNextUrl) && iCounter < iMaxPage) { _xProgress?.Report(new WebHandlerProgress(xDriver.Url, true)); - var (arrSubPages, sUrl, sNext) = WorkMainPage(dPages, arrItems, sAnnouncementType); + List arrSubPages; + string sUrl; + string sNext; + try + { + (arrSubPages, sUrl, sNext) = WorkMainPage(dPages, arrItems, sAnnouncementType); + } + catch + { + (arrSubPages, sUrl, sNext) = WorkMainPage(dPages, arrItems, sAnnouncementType); + } sNextUrl = sNext; - _xProgress?.Report(new WebHandlerProgress("\nGet Subpages:")); + _xProgress?.Report(new WebHandlerProgress(UiProgressGetSubPages)); foreach (var sSubPage in arrSubPages) { - xDriver.Navigate().GoToUrl(sSubPage + "/anzeigen"); - Thread.Sleep(5500); + var sSubPageUrl = sSubPage + PagePathAnzeigen; + if (_hsIndexedSubPages.Contains(sSubPageUrl)) + { + continue; + } + + if (!NavigateWithReloadRetry(sSubPageUrl)) + { + continue; + } + WorkSubPage(sUrl, dPages, arrItems); + _hsIndexedSubPages.Add(sSubPageUrl); } _xProgress?.Report(new WebHandlerProgress(string.Empty, true)); if (!string.IsNullOrEmpty(sNextUrl)) { - xDriver.Navigate().GoToUrl(sNextUrl); - while (xDriver.Url == sUrl) + if (!NavigateWithReloadRetry(sNextUrl)) + { + sNextUrl = string.Empty; + } + else { - Thread.Sleep(500); - _xProgress?.Report(new WebHandlerProgress(".")); + while (xDriver.Url == sUrl) + { + Thread.Sleep(NavigationWaitMilliseconds); + _xProgress?.Report(new WebHandlerProgress(UiProgressDot)); + } } } @@ -194,77 +338,79 @@ public void Dispose() private (List SubPages, string Url, string NextUrl) WorkMainPage(Dictionary> dPages, List> arrItems, string sAnnouncementType) { - var xDriver = Driver ?? throw new InvalidOperationException("The web driver has not been initialized."); + var xDriver = Driver ?? throw new InvalidOperationException(MissingDriverMessage); var sUrl = xDriver.Url; var dPage = new Dictionary { - ["Title"] = xDriver.Title, - ["url"] = sUrl, - ["filter"] = sAnnouncementType, - ["sections"] = new List>(), - ["content"] = PortedHelpers.MakeLocal(xDriver.PageSource, sUrl.Length > 50 ? sUrl[..50] + "/" : sUrl + "/") + [KeyTitle] = xDriver.Title, + [KeyUrl] = sUrl, + [KeyFilter] = sAnnouncementType, + [KeySections] = new List>(), + [KeyContent] = PortedHelpers.MakeLocal(xDriver.PageSource, sUrl.Length > 50 ? sUrl[..50] + PagePathSeparator : sUrl + PagePathSeparator) }; var arrSubPages = new List(); dPages[sUrl] = dPage; - var arrElements = Wdr2List(xDriver, new WebQuery(string.Empty, By.ClassName("c-blockitem"), new WebQuery("links", By.TagName("a")), new WebQuery("imgs", By.TagName("img")))); - dPage["sections"] = arrElements; + var arrElements = Wdr2List(xDriver, new WebQuery(string.Empty, By.ClassName(PageBlockItemClass), new WebQuery(KeyLinks, By.TagName(HtmlTagA)), new WebQuery(KeyImages, By.TagName(HtmlTagImage)))); + dPage[KeySections] = arrElements; var sNextUrl = string.Empty; foreach (var dElement in arrElements) { - var arrText = GetStringList(dElement, "text"); - if (arrText.Count > 1 && arrText[0].StartsWith("ANZ", StringComparison.Ordinal)) + var arrText = GetStringList(dElement, KeyText); + if (arrText.Count > 1 && arrText[0].StartsWith(AnnouncementPrefix, StringComparison.Ordinal)) { var dAnnouncement = new Dictionary { - ["Title"] = arrText[0].Length >= 8 ? arrText[0][8..] : arrText[0], - ["parent"] = sUrl, - ["Text"] = arrText.Cast().ToList() + [KeyTitle] = arrText[0].Length >= 8 ? arrText[0][8..] : arrText[0], + [KeyParent] = sUrl, + [KeyText] = arrText.Cast().ToList() }; if (arrText.Count > 1) { - dAnnouncement["Info"] = arrText[1]; + dAnnouncement[KeyInfo] = arrText[1]; } - var arrLinks = GetDictionaryList(dElement, "links"); + var arrLinks = GetDictionaryList(dElement, KeyLinks); if (arrLinks.Count > 0) { - var sHref = Convert.ToString(arrLinks[0].GetValueOrDefault("href"), CultureInfo.InvariantCulture) ?? string.Empty; - dAnnouncement["href"] = sHref; + var sHref = Convert.ToString(arrLinks[0].GetValueOrDefault(KeyHref), CultureInfo.InvariantCulture) ?? string.Empty; + dAnnouncement[KeyHref] = sHref; arrSubPages.Add(sHref); } - var arrImages = GetDictionaryList(dElement, "imgs"); + var arrImages = GetDictionaryList(dElement, KeyImages); if (arrImages.Count > 0) { foreach (var dImage in arrImages) { var dCopy = new Dictionary(dAnnouncement, StringComparer.Ordinal); dCopy[CsSrc] = string.Empty; - var sSource = Convert.ToString(dImage.GetValueOrDefault("src"), CultureInfo.InvariantCulture) ?? string.Empty; - var sDataOriginal = Convert.ToString(dImage.GetValueOrDefault("data-original"), CultureInfo.InvariantCulture) ?? string.Empty; - if (sSource.Contains("MEDIASERVER", StringComparison.Ordinal)) + var sSource = Convert.ToString(dImage.GetValueOrDefault(CsSrc), CultureInfo.InvariantCulture) ?? string.Empty; + if (string.IsNullOrEmpty(sSource)) + { + sSource = Convert.ToString(dImage.GetValueOrDefault(HtmlAttrSrc), CultureInfo.InvariantCulture) ?? string.Empty; + } + + var sDataOriginal = Convert.ToString(dImage.GetValueOrDefault(DataAttributeName), CultureInfo.InvariantCulture) ?? string.Empty; + if (sSource.Contains(MediaServerToken, StringComparison.Ordinal)) { dCopy[CsSrc] = sSource; } - else if (sDataOriginal.Contains("MEDIASERVER", StringComparison.Ordinal)) + else if (sDataOriginal.Contains(MediaServerToken, StringComparison.Ordinal)) { dCopy[CsSrc] = sDataOriginal; } - _xProgress?.Report(new WebHandlerProgress(".")); + _xProgress?.Report(new WebHandlerProgress(UiProgressDot)); try { var sMediaSource = Convert.ToString(dCopy[CsSrc], CultureInfo.InvariantCulture) ?? string.Empty; if (!string.IsNullOrEmpty(sMediaSource)) { - var xResponse = _xHttpClient.GetAsync(sMediaSource).GetAwaiter().GetResult(); - dCopy[CsData] = xResponse.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult(); - dCopy[CsHeader] = xResponse.Headers.Concat(xResponse.Content.Headers) - .ToDictionary(h => h.Key, h => string.Join(",", h.Value), StringComparer.OrdinalIgnoreCase); - if (sMediaSource.Contains("MEDIASERVER", StringComparison.Ordinal)) + TryLoadBinary(sMediaSource, dCopy); + if (sMediaSource.Contains(MediaServerToken, StringComparison.Ordinal)) { dPage[sMediaSource] = new Dictionary { @@ -286,15 +432,15 @@ public void Dispose() arrItems.Add(dAnnouncement); } } - else if (arrText.Count > 1 && string.Join(" ", arrText).Contains('>') && string.IsNullOrEmpty(sNextUrl)) + else if (arrText.Count > 1 && string.Join(SpaceSeparator, arrText).Contains(NextLinkMarker, StringComparison.Ordinal) && string.IsNullOrEmpty(sNextUrl)) { - var arrLinks = GetDictionaryList(dElement, "links"); + var arrLinks = GetDictionaryList(dElement, KeyLinks); foreach (var dLink in arrLinks) { - var arrLinkText = GetStringList(dLink, "text"); - if (arrLinkText.Count == 1 && arrLinkText[0] == ">") + var arrLinkText = GetStringList(dLink, KeyText); + if (arrLinkText.Count == 1 && arrLinkText[0] == NextLinkMarker) { - sNextUrl = Convert.ToString(dLink.GetValueOrDefault("href"), CultureInfo.InvariantCulture) ?? string.Empty; + sNextUrl = Convert.ToString(dLink.GetValueOrDefault(KeyHref), CultureInfo.InvariantCulture) ?? string.Empty; } } @@ -307,101 +453,106 @@ public void Dispose() private void WorkSubPage(string sUrl, Dictionary> dPages, List> arrItems) { - var xDriver = Driver ?? throw new InvalidOperationException("The web driver has not been initialized."); + var xDriver = Driver ?? throw new InvalidOperationException(MissingDriverMessage); var dPage = dPages[sUrl]; var sSubPageUrl = xDriver.Url; if (!dPages.TryGetValue(sSubPageUrl, out var dPage2)) { dPage2 = new Dictionary { - ["parent"] = new List() + [KeyParent] = new List() }; dPages[sSubPageUrl] = dPage2; } Dictionary dParentSection = new(StringComparer.Ordinal); - var arrPageSections = GetDictionaryList(dPage, "sections"); + var arrPageSections = GetDictionaryList(dPage, KeySections); for (var iIndex = 0; iIndex < arrPageSections.Count; iIndex++) { var dSection = arrPageSections[iIndex]; - var arrLinks = GetDictionaryList(dSection, "links"); - var sHref = arrLinks.Count > 0 ? Convert.ToString(arrLinks[0].GetValueOrDefault("href"), CultureInfo.InvariantCulture) ?? string.Empty : string.Empty; - if (!string.IsNullOrEmpty(sHref) && sHref + "/anzeigen" == sSubPageUrl) + var arrLinks = GetDictionaryList(dSection, KeyLinks); + var sHref = arrLinks.Count > 0 ? Convert.ToString(arrLinks[0].GetValueOrDefault(KeyHref), CultureInfo.InvariantCulture) ?? string.Empty : string.Empty; + if (!string.IsNullOrEmpty(sHref) && sHref + PagePathAnzeigen == sSubPageUrl) { dParentSection = dSection; arrPageSections[iIndex] = dSection; } } - dPage2["Title"] = xDriver.Title; - dPage2["url"] = sSubPageUrl; - if (dPage2["parent"] is not List arrParents) + dPage2[KeyTitle] = xDriver.Title; + dPage2[KeyUrl] = sSubPageUrl; + if (dPage2[KeyParent] is not List arrParents) { arrParents = new List(); - dPage2["parent"] = arrParents; + dPage2[KeyParent] = arrParents; } arrParents.Add(sUrl); - dPage2["content"] = PortedHelpers.MakeLocal(xDriver.PageSource, sSubPageUrl + '/'); + dPage2[KeyContent] = PortedHelpers.MakeLocal(xDriver.PageSource, sSubPageUrl + PagePathSeparator); var arrMedia = new List(); - dPage2["media"] = arrMedia; - var arrSections = Wdr2List(xDriver, new WebQuery(string.Empty, By.TagName("section"), new WebQuery("links", By.TagName("a")), new WebQuery("imgs", By.TagName("img")))); - dPage2["sections"] = arrSections; + dPage2[KeyMedia] = arrMedia; + var arrSections = Wdr2List(xDriver, new WebQuery(string.Empty, By.TagName(HtmlTagSection), new WebQuery(KeyLinks, By.TagName(HtmlTagA)), new WebQuery(KeyImages, By.TagName(HtmlTagImage)))); + dPage2[KeySections] = arrSections; - foreach (var xElement in xDriver.FindElements(By.TagName("h1"))) + foreach (var xElement in xDriver.FindElements(By.TagName(HtmlTagHeading1))) { - dPage2["name"] = xElement.Text; + dPage2[KeyName] = xElement.Text; } - foreach (var xElement in xDriver.FindElements(By.ClassName("col-sm-6"))) + foreach (var xElement in xDriver.FindElements(By.ClassName(HtmlClassDetailColumn))) { - if (xElement.Text.StartsWith("*", StringComparison.Ordinal)) + if (xElement.Text.StartsWith(BirthMarker, StringComparison.Ordinal)) { - dPage2["Birth"] = xElement.Text; + dPage2[KeyBirth] = xElement.Text; } else if (xElement.Text.Contains('†')) { - var arrParts = xElement.Text.Split("in", 2, StringSplitOptions.None); - dPage2["Death"] = arrParts[0]; - dPage2["Place"] = arrParts.Length > 1 ? arrParts[1] : string.Empty; + var arrParts = xElement.Text.Split(PlaceSeparator, 2, StringSplitOptions.None); + dPage2[KeyDeath] = arrParts[0]; + dPage2[KeyPlace] = arrParts.Length > 1 ? arrParts[1] : string.Empty; } } - _xProgress?.Report(new WebHandlerProgress(".")); + _xProgress?.Report(new WebHandlerProgress(UiProgressDot)); foreach (var dSection in arrSections) { - var sSectionClass = Convert.ToString(dSection.GetValueOrDefault("class"), CultureInfo.InvariantCulture) ?? string.Empty; - if (sSectionClass.StartsWith("col-12", StringComparison.Ordinal)) + var sSectionClass = Convert.ToString(dSection.GetValueOrDefault(HtmlAttrClass), CultureInfo.InvariantCulture) ?? string.Empty; + if (sSectionClass.StartsWith(HtmlClassContentColumn, StringComparison.Ordinal)) { - foreach (var sLine in GetStringList(dSection, "text")) + foreach (var sLine in GetStringList(dSection, KeyText)) { - if (sLine.StartsWith("Erstellt", StringComparison.Ordinal)) + if (sLine.StartsWith(UiLabelCreated, StringComparison.Ordinal)) { - dPage2["created_by"] = sLine.Length > 12 ? sLine[12..] : string.Empty; + dPage2[KeyCreatedBy] = sLine.Length > 12 ? sLine[12..] : string.Empty; } - else if (sLine.StartsWith("Angelegt", StringComparison.Ordinal)) + else if (sLine.StartsWith(UiLabelCreatedOn, StringComparison.Ordinal)) { - dPage2["created_on"] = sLine.Length > 11 ? sLine[11..] : string.Empty; + dPage2[KeyCreatedOn] = sLine.Length > 11 ? sLine[11..] : string.Empty; } - else if (sLine.EndsWith("Besuche", StringComparison.Ordinal)) + else if (sLine.EndsWith(UiLabelVisits, StringComparison.Ordinal)) { - dPage2["visits"] = sLine.Length > 7 ? sLine[..^7] : string.Empty; + dPage2[KeyVisits] = sLine.Length > 7 ? sLine[..^7] : string.Empty; } } - foreach (var dImage in GetDictionaryList(dSection, "imgs")) + foreach (var dImage in GetDictionaryList(dSection, KeyImages)) { - var sHref = Convert.ToString(dImage.GetValueOrDefault("src"), CultureInfo.InvariantCulture) ?? string.Empty; - var sText = Convert.ToString(dImage.GetValueOrDefault("alt"), CultureInfo.InvariantCulture) ?? string.Empty; - if (sHref.Contains("MEDIASERVER", StringComparison.Ordinal) && !arrMedia.Contains(sHref)) + var sHref = Convert.ToString(dImage.GetValueOrDefault(CsSrc), CultureInfo.InvariantCulture) ?? string.Empty; + if (string.IsNullOrEmpty(sHref)) + { + sHref = Convert.ToString(dImage.GetValueOrDefault(HtmlAttrSrc), CultureInfo.InvariantCulture) ?? string.Empty; + } + + var sText = Convert.ToString(dImage.GetValueOrDefault(HtmlAttrAlt), CultureInfo.InvariantCulture) ?? string.Empty; + if (sHref.Contains(MediaServerToken, StringComparison.Ordinal) && !arrMedia.Contains(sHref)) { arrMedia.Add(sHref); var dAnnouncement = new Dictionary { - ["parent"] = sSubPageUrl, + [KeyParent] = sSubPageUrl, [CsSrc] = sHref, - ["Info"] = sText, - ["id"] = string.Empty + [KeyInfo] = sText, + [KeyId] = string.Empty }; TryLoadBinary(sHref, dAnnouncement); @@ -409,68 +560,73 @@ private void WorkSubPage(string sUrl, Dictionary dPageMedia) { - dPageMedia["id-anz"] = dParentSection["id-anz"]; + dPageMedia[KeyIdAnz] = dParentSection[KeyIdAnz]; } - dSection["filter"] = dPage["filter"]; + dSection[KeyFilter] = dPage[KeyFilter]; } - else if (!string.IsNullOrEmpty(sDataOriginal) && dPage.ContainsKey($"https://trauer.rnz.de{sDataOriginal}")) + else if (!string.IsNullOrEmpty(sDataOriginal) && dPage.ContainsKey($"{_sBaseHost}{sDataOriginal}")) { - dParentSection["id-anz"] = Convert.ToString(dSection.GetValueOrDefault("id"), CultureInfo.InvariantCulture) ?? string.Empty; - if (dPage[$"https://trauer.rnz.de{sDataOriginal}"] is Dictionary dPageMedia) + dParentSection[KeyIdAnz] = Convert.ToString(dSection.GetValueOrDefault(KeyId), CultureInfo.InvariantCulture) ?? string.Empty; + if (dPage[$"{_sBaseHost}{sDataOriginal}"] is Dictionary dPageMedia) { - dPageMedia["id-anz"] = dParentSection["id-anz"]; + dPageMedia[KeyIdAnz] = dParentSection[KeyIdAnz]; } - dSection["filter"] = dPage["filter"]; + dSection[KeyFilter] = dPage[KeyFilter]; } } - foreach (var dLink in GetDictionaryList(dSection, "links")) + foreach (var dLink in GetDictionaryList(dSection, KeyLinks)) { try { - var sHref = Convert.ToString(dLink.GetValueOrDefault("href"), CultureInfo.InvariantCulture) ?? string.Empty; - var arrText = GetStringList(dLink, "text"); - if (sHref.Contains("MEDIASERVER", StringComparison.Ordinal) && !arrMedia.Contains(sHref)) + var sHref = Convert.ToString(dLink.GetValueOrDefault(KeyHref), CultureInfo.InvariantCulture) ?? string.Empty; + var arrText = GetStringList(dLink, KeyText); + if (sHref.Contains(MediaServerToken, StringComparison.Ordinal) && !arrMedia.Contains(sHref)) { arrMedia.Add(sHref); } - if ((arrText.Count == 1 && arrText[0] == "Speichern") || (arrText.Count == 1 && arrText[0] == "Großansicht")) + if ((arrText.Count == 1 && arrText[0] == UiActionSave) || (arrText.Count == 1 && arrText[0] == UiActionLargeView)) { var dAnnouncement = new Dictionary { - ["parent"] = sSubPageUrl, + [KeyParent] = sSubPageUrl, [CsSrc] = sHref, - ["id"] = Convert.ToString(dSection.GetValueOrDefault("id"), CultureInfo.InvariantCulture) ?? string.Empty + [KeyId] = Convert.ToString(dSection.GetValueOrDefault(KeyId), CultureInfo.InvariantCulture) ?? string.Empty }; - if (Equals(dParentSection.GetValueOrDefault("id-anz"), dAnnouncement["id"])) + if (Equals(dParentSection.GetValueOrDefault(KeyIdAnz), dAnnouncement[KeyId])) { - var arrImages = GetDictionaryList(dParentSection, "imgs"); + var arrImages = GetDictionaryList(dParentSection, KeyImages); arrImages.Add(new Dictionary(dAnnouncement)); - dParentSection["imgs"] = arrImages; + dParentSection[KeyImages] = arrImages; } TryLoadBinary(sHref, dAnnouncement); - if (sHref.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) && dAnnouncement.TryGetValue(CsData, out var xDataObject) && xDataObject is byte[] arrBinary) + if (sHref.EndsWith(ImageFileJpg, StringComparison.OrdinalIgnoreCase) && dAnnouncement.TryGetValue(CsData, out var xDataObject) && xDataObject is byte[] arrBinary) { var sPrefix = Encoding.ASCII.GetString(arrBinary.Take(10).ToArray()); - if (sPrefix.Contains("PNG", StringComparison.Ordinal)) + if (sPrefix.Contains(ImageSignaturePng, StringComparison.Ordinal)) { - dLink["href"] = sHref.Replace(".jpg", ".png", StringComparison.OrdinalIgnoreCase); + dLink[KeyHref] = sHref.Replace(ImageFileJpg, ImageFilePng, StringComparison.OrdinalIgnoreCase); } } @@ -487,17 +643,33 @@ private void WorkSubPage(string sUrl, Dictionary dTarget) { + if (_dIndexedBinaryData.TryGetValue(sHref, out var arrIndexedData)) + { + dTarget[CsData] = arrIndexedData; + dTarget[CsHeader] = _dIndexedBinaryHeaders.TryGetValue(sHref, out var dIndexedHeaders) + ? new Dictionary(dIndexedHeaders, StringComparer.OrdinalIgnoreCase) + : new Dictionary(StringComparer.OrdinalIgnoreCase); + return; + } + try { var xResponse = _xHttpClient.GetAsync(sHref).GetAwaiter().GetResult(); - dTarget[CsData] = xResponse.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult(); - dTarget[CsHeader] = xResponse.Headers.Concat(xResponse.Content.Headers) - .ToDictionary(h => h.Key, h => string.Join(",", h.Value), StringComparer.OrdinalIgnoreCase); + var arrData = xResponse.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult(); + var dHeaders = xResponse.Headers.Concat(xResponse.Content.Headers) + .ToDictionary(h => h.Key, h => string.Join(CommaSeparator, h.Value), StringComparer.OrdinalIgnoreCase); + + dTarget[CsData] = arrData; + dTarget[CsHeader] = dHeaders; + _dIndexedBinaryData[sHref] = arrData; + _dIndexedBinaryHeaders[sHref] = new Dictionary(dHeaders, StringComparer.OrdinalIgnoreCase); } catch { dTarget[CsData] = Array.Empty(); dTarget[CsHeader] = new Dictionary(StringComparer.OrdinalIgnoreCase); + _dIndexedBinaryData[sHref] = Array.Empty(); + _dIndexedBinaryHeaders[sHref] = new Dictionary(StringComparer.OrdinalIgnoreCase); } } @@ -530,4 +702,108 @@ private static List GetStringList(IReadOnlyDictionary d _ => [] }; } + + private bool NavigateWithReloadRetry(string sUrl) + { + var xDriver = Driver ?? throw new InvalidOperationException(MissingDriverMessage); + var iAttempt = 0; + while (iAttempt < MaxReloadAttempts) + { + iAttempt++; + try + { + xDriver.Navigate().GoToUrl(sUrl); + } + catch (WebDriverException) + { + _xProgress.Report(new WebHandlerProgress($"Navigation attempt {iAttempt} to {sUrl} failed with WebDriverException.")); + Thread.Sleep(3000); + continue; + } + WaitForPageLoadCompletion(xDriver); + if (HasMeaningfulPageContent(xDriver)) + { + return true; + } + _xProgress.Report(new WebHandlerProgress("O")); + Thread.Sleep(30000); + } + + return false; + } + + private static void WaitForPageLoadCompletion(IWebDriver xDriver) + { + var iCycle = 0; + while ((xDriver.Title == PageTitleRnz || string.IsNullOrEmpty(xDriver.Title)) && iCycle < NavigationWaitCycles) + { + Thread.Sleep(NavigationWaitMilliseconds); + iCycle++; + } + + Thread.Sleep(NavigationWaitMilliseconds); + } + + private static bool HasMeaningfulPageContent(IWebDriver xDriver) + { + var sTitle = xDriver.Title ?? string.Empty; + var sBodyText = TryGetBodyText(xDriver); + + if (string.IsNullOrWhiteSpace(sTitle) && string.IsNullOrWhiteSpace(sBodyText)) + { + return false; + } + + return !ContainsErrorMarker(sTitle) && !ContainsErrorMarker(sBodyText); + } + + private static string TryGetBodyText(ISearchContext xSearchContext) + { + try + { + var arrBodies = xSearchContext.FindElements(By.TagName(HtmlTagBody)); + foreach (var xBody in arrBodies) + { + try + { + return xBody.Text ?? string.Empty; + } + catch (StaleElementReferenceException) + { + } + } + } + catch (WebDriverException) + { + } + + return string.Empty; + } + + private static bool ContainsErrorMarker(string sText) + { + if (string.IsNullOrWhiteSpace(sText)) + { + return false; + } + + var sNormalized = sText.ToLowerInvariant() + .Replace('’', '\'') + .Replace('‑', '-') + .Replace('–', '-'); + + return sNormalized.Contains(ErrorMarker404, StringComparison.Ordinal) && sNormalized.Length < 500 + || sNormalized.Contains(ErrorMarkerNotFound, StringComparison.Ordinal) + || sNormalized.Contains(ErrorMarkerServiceTimeout, StringComparison.Ordinal) + || sNormalized.Contains(ErrorMarkerServiceUnavailable, StringComparison.Ordinal) + || sNormalized.Contains(ErrorMarkerTemporarilyUnavailable, StringComparison.Ordinal) + || sNormalized.Contains(ErrorMarkerBadGateway, StringComparison.Ordinal) + || sNormalized.Contains(ErrorMarkerGatewayTimeout, StringComparison.Ordinal) + || sNormalized.Contains(ErrorMarker504GatewayTimeOut, StringComparison.Ordinal) + || sNormalized.Contains(ErrorMarkerServerDidntRespondInTime, StringComparison.Ordinal) + || sNormalized.Contains(ErrorMarkerServerDidNotRespondInTime, StringComparison.Ordinal) + || sNormalized.Contains(ErrorMarkerRequestTimeout, StringComparison.Ordinal) + || sNormalized.Contains(ErrorMarkerConnectionTimeout, StringComparison.Ordinal) + || sNormalized.Contains(ErrorMarkerOperationTimedOut, StringComparison.Ordinal); + } } From 110337427f116a616cd6409ee990f6b9512c005b Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:06:48 +0200 Subject: [PATCH 72/96] RnzTrauer.Tests --- .../RnzTrauer.Tests/WebHandlerTests.cs | 118 +++++++++++++++++- 1 file changed, 117 insertions(+), 1 deletion(-) diff --git a/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/WebHandlerTests.cs b/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/WebHandlerTests.cs index 0fb42f3cf..2473b1ee6 100644 --- a/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/WebHandlerTests.cs +++ b/WinAhnenNew/RnzTrauer/RnzTrauer.Tests/WebHandlerTests.cs @@ -185,7 +185,7 @@ public void GetData1_Loads_Announcement_Media_Using_Injected_Dependencies() Assert.IsTrue(lstItems.All(dItem => dItem.ContainsKey(WebHandler.CsData))); Assert.IsTrue(lstItems.All(dItem => dItem.ContainsKey(WebHandler.CsHeader))); Assert.IsTrue(dPages.Values.All(dPage => dPage.ContainsKey("https://trauer.rnz.de/MEDIASERVER/image.jpg"))); - _ = xHttpClient.Received(4).GetAsync("https://trauer.rnz.de/MEDIASERVER/image.jpg"); + _ = xHttpClient.Received(1).GetAsync("https://trauer.rnz.de/MEDIASERVER/image.jpg"); xProgress.Received().Report(new WebHandlerProgress(".")); } @@ -401,6 +401,17 @@ public void WorkSubPage_Extracts_Metadata_And_Media_Via_Reflection() Assert.AreEqual("https://example.invalid/subpage/anzeigen", lstItems[0]["parent"]); } + [DataTestMethod] + [DataRow("ok", false)] + [DataRow("504 gateway time-out", true)] + [DataRow("Service Unavailable", true)] + public void ContainsErrorMarker_Detects_Only_Real_Error_Texts(string sText, bool xExpected) + { + var xResult = InvokeNonPublicStaticMethod(typeof(WebHandler), "ContainsErrorMarker", sText); + + Assert.AreEqual(xExpected, xResult); + } + private static ReadOnlyCollection CreateCollection(params IWebElement[] arrElements) { return new ReadOnlyCollection(arrElements.ToList()); @@ -455,4 +466,109 @@ private static T InvokeNonPublicStaticMethod(Type xType, string sMethodName, Assert.IsNotNull(xMethod); return (T)xMethod.Invoke(null, arrArguments)!; } + + [TestMethod] + public void TryLoadBinary_CacheHit_Reuses_Same_ByteArray_Instance_Via_Reflection() + { + var xHttpClient = Substitute.For(); + var xResponse = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new ByteArrayContent([1, 2, 3]) + }; + xResponse.Headers.Add("X-Test", "1"); + xHttpClient.GetAsync("https://example.invalid/cached.bin").Returns(Task.FromResult(xResponse)); + using var xHandler = CreateHandler(xHttpClient: xHttpClient); + + var dTarget1 = new Dictionary(); + var dTarget2 = new Dictionary(); + + InvokeNonPublicInstanceMethod(xHandler, "TryLoadBinary", "https://example.invalid/cached.bin", dTarget1); + InvokeNonPublicInstanceMethod(xHandler, "TryLoadBinary", "https://example.invalid/cached.bin", dTarget2); + + var arrData1 = (byte[])dTarget1[WebHandler.CsData]!; + var arrData2 = (byte[])dTarget2[WebHandler.CsData]!; + Assert.IsTrue(ReferenceEquals(arrData1, arrData2)); + _ = xHttpClient.Received(1).GetAsync("https://example.invalid/cached.bin"); + } + + [TestMethod] + public void TryLoadBinary_CacheHit_Returns_Copied_Header_Dictionary_Via_Reflection() + { + var xHttpClient = Substitute.For(); + var xResponse = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new ByteArrayContent([1, 2, 3]) + }; + xResponse.Headers.Add("X-Test", "1"); + xHttpClient.GetAsync("https://example.invalid/cached-headers.bin").Returns(Task.FromResult(xResponse)); + using var xHandler = CreateHandler(xHttpClient: xHttpClient); + + var dTarget1 = new Dictionary(); + var dTarget2 = new Dictionary(); + + InvokeNonPublicInstanceMethod(xHandler, "TryLoadBinary", "https://example.invalid/cached-headers.bin", dTarget1); + var dHeaders1 = (Dictionary)dTarget1[WebHandler.CsHeader]!; + dHeaders1["X-Test"] = "modified"; + + InvokeNonPublicInstanceMethod(xHandler, "TryLoadBinary", "https://example.invalid/cached-headers.bin", dTarget2); + var dHeaders2 = (Dictionary)dTarget2[WebHandler.CsHeader]!; + + Assert.IsFalse(ReferenceEquals(dHeaders1, dHeaders2)); + Assert.AreEqual("1", dHeaders2["X-Test"]); + } + + [TestMethod] + public void HasMeaningfulPageContent_Does_Not_Read_PageSource() + { + var xDriver = Substitute.For(); + var xBody = Substitute.For(); + + xDriver.Title.Returns("Traueranzeigen von Manfred Hindemith | Trauer.rnz.de"); + xDriver.FindElements(Arg.Is(xBy => xBy.ToString() == By.TagName("body").ToString())).Returns(CreateCollection(xBody)); + xBody.Text.Returns("Seiteninhalt"); + xDriver.PageSource.Returns(_ => throw new WebDriverException("timeout")); + + var xResult = InvokeNonPublicStaticMethod(typeof(WebHandler), "HasMeaningfulPageContent", xDriver); + + Assert.IsTrue(xResult); + } + + [TestMethod] + public void HasMeaningfulPageContent_Returns_False_For_Error_Text_In_Body() + { + var xDriver = Substitute.For(); + var xBody = Substitute.For(); + + xDriver.Title.Returns("Loaded"); + xDriver.FindElements(Arg.Is(xBy => xBy.ToString() == By.TagName("body").ToString())).Returns(CreateCollection(xBody)); + xBody.Text.Returns("504 gateway time-out"); + + var xResult = InvokeNonPublicStaticMethod(typeof(WebHandler), "HasMeaningfulPageContent", xDriver); + + Assert.IsFalse(xResult); + } + + [TestMethod] + public void TryLoadBinary_Mutating_Returned_Cached_ByteArray_Affects_Followup_Reads_Via_Reflection() + { + var xHttpClient = Substitute.For(); + var xResponse = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new ByteArrayContent([1, 2, 3]) + }; + xHttpClient.GetAsync("https://example.invalid/mutable-cache.bin").Returns(Task.FromResult(xResponse)); + using var xHandler = CreateHandler(xHttpClient: xHttpClient); + + var dTarget1 = new Dictionary(); + var dTarget2 = new Dictionary(); + + InvokeNonPublicInstanceMethod(xHandler, "TryLoadBinary", "https://example.invalid/mutable-cache.bin", dTarget1); + var arrData1 = (byte[])dTarget1[WebHandler.CsData]!; + arrData1[0] = 9; + + InvokeNonPublicInstanceMethod(xHandler, "TryLoadBinary", "https://example.invalid/mutable-cache.bin", dTarget2); + var arrData2 = (byte[])dTarget2[WebHandler.CsData]!; + + Assert.AreEqual(9, arrData2[0]); + } } From 3d272e0a46407aa9d58c5747cd58110acd7d0772 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:06:49 +0200 Subject: [PATCH 73/96] GenFreeWin --- GenFreeWin/.github/copilot-instructions.md | 38 +++++++++++++------ .../RnzTrauer/Directory.Packages.props | 2 +- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/GenFreeWin/.github/copilot-instructions.md b/GenFreeWin/.github/copilot-instructions.md index 4f86cf3d7..77988b00d 100644 --- a/GenFreeWin/.github/copilot-instructions.md +++ b/GenFreeWin/.github/copilot-instructions.md @@ -5,8 +5,11 @@ Apply these defaults when working in this repository unless the user explicitly ## General Guidelines - Document code thoroughly in English. - Validate changes with relevant builds and tests before finishing. -- If requirements are unclear, ask clarifying questions before starting implementation. -- Respect strict nullability to indicate when a variable can be null, and handle nullability appropriately in code. +- If requirements are unclear, ask clarifying questions before starting implementation or planning refinement. +- Avoid UI text strings in core services. Use Enumerations instead, and keep UI-facing strings in the ViewModel/UI layer. +- Prefer one class/interface/struct per file. +- document changes in an DevOps-manner markdown prefered, extrapolate bugs, tasks, baglogs and features +- Use `DevOps` as the planning directory in this workspace, and treat `.Info.md` as the general planning description file. Team terminology around Azure DevOps backlog items may differ from generic 'story' naming. ## Testing - Use `MSTest` in the latest practical version for new or updated tests. @@ -18,17 +21,28 @@ Apply these defaults when working in this repository unless the user explicitly ## Architecture - Use MVVM architecture for UI components to separate concerns and improve testability, using CommunityToolkit.Mvvm for MVVM implementation. +- Prefer `NotifyPropertyChangedFor` over manual `OnPropertyChanged(nameof(...))` in CommunityToolkit.Mvvm observable properties where applicable. - Use Dependency Injection to manage dependencies and improve testability, using Microsoft.Extensions.DependencyInjection. -- CommunityToolkit.Mvvm source-generator-based view models are valid; editor errors can disappear after a build, so do not replace them prematurely with manual implementations just because generated members are not yet available in design-time analysis. +- UI-facing strings and summary formatting should stay in the ViewModel/UI layer, not in extracted application logic services. ## Naming Conventions +- Distinguish between UI control naming and variable/field naming. - Use PascalCase for class names, method names, and properties. -- Use _camelCase for local/private variables and parameters. -- Use 1-3 letter prefixes for type of variable, e.g. - - `s` for string, - - `i` for all int (8-128bit), - - `u` for Unsigned Int (8-128 bit), - - `x` for bool, - - `f` for float/double, - - `lst` for list, - - `btn` for button, etc. +- Use `_camelCase` for private fields. +- Use `camelCase` for local variables and parameters. +- Use short 1-character prefixes for simple types only when they meaningfully disambiguate, e.g. + - `s` for `string` + - `i` for signed integer types + - `u` for unsigned integer types + - `x` for `bool` + - `f` for `float`, `double`, or `decimal` +- Prefer meaningful domain names over type prefixes when the intent is already clear. +- In UI code, use short 3-character prefixes for actual controls in views and code-behind, e.g. + - `lst` for list controls + - `btn` for all kind of buttons, + - `edt` for any keyboard input control + - `lbl` for any text output control +- Do not use UI control prefixes for ViewModel properties or other non-UI members. + +## Nullability +- Use strict nullable reference types to indicate when a variable can be null, and handle nullability appropriately in code. diff --git a/WinAhnenNew/RnzTrauer/Directory.Packages.props b/WinAhnenNew/RnzTrauer/Directory.Packages.props index a66e44c5d..f8c24d522 100644 --- a/WinAhnenNew/RnzTrauer/Directory.Packages.props +++ b/WinAhnenNew/RnzTrauer/Directory.Packages.props @@ -4,7 +4,7 @@ - + From 36dbac49c8357f236001f61bf7a4dbda3dca1052 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:10:20 +0200 Subject: [PATCH 74/96] AppWithPlugin --- .../.github/copilot-instructions.md | 48 +++++++++++++++++++ .../.github/upgrades/dotnet-upgrade-plan.md | 33 +++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 TestStatements/.github/copilot-instructions.md create mode 100644 TestStatements/.github/upgrades/dotnet-upgrade-plan.md diff --git a/TestStatements/.github/copilot-instructions.md b/TestStatements/.github/copilot-instructions.md new file mode 100644 index 000000000..77988b00d --- /dev/null +++ b/TestStatements/.github/copilot-instructions.md @@ -0,0 +1,48 @@ +# Repository instructions for GitHub Copilot + +Apply these defaults when working in this repository unless the user explicitly asks otherwise: + +## General Guidelines +- Document code thoroughly in English. +- Validate changes with relevant builds and tests before finishing. +- If requirements are unclear, ask clarifying questions before starting implementation or planning refinement. +- Avoid UI text strings in core services. Use Enumerations instead, and keep UI-facing strings in the ViewModel/UI layer. +- Prefer one class/interface/struct per file. +- document changes in an DevOps-manner markdown prefered, extrapolate bugs, tasks, baglogs and features +- Use `DevOps` as the planning directory in this workspace, and treat `.Info.md` as the general planning description file. Team terminology around Azure DevOps backlog items may differ from generic 'story' naming. + +## Testing +- Use `MSTest` in the latest practical version for new or updated tests. +- Use `NSubstitute` for mocks, stubs, and substitutes in tests. +- Prefer `DataRow` for parameterized single-test scenarios. + +## Internationalization +- Keep I18N in mind when writing code, ensuring it can be easily adapted for different languages and regions. + +## Architecture +- Use MVVM architecture for UI components to separate concerns and improve testability, using CommunityToolkit.Mvvm for MVVM implementation. +- Prefer `NotifyPropertyChangedFor` over manual `OnPropertyChanged(nameof(...))` in CommunityToolkit.Mvvm observable properties where applicable. +- Use Dependency Injection to manage dependencies and improve testability, using Microsoft.Extensions.DependencyInjection. +- UI-facing strings and summary formatting should stay in the ViewModel/UI layer, not in extracted application logic services. + +## Naming Conventions +- Distinguish between UI control naming and variable/field naming. +- Use PascalCase for class names, method names, and properties. +- Use `_camelCase` for private fields. +- Use `camelCase` for local variables and parameters. +- Use short 1-character prefixes for simple types only when they meaningfully disambiguate, e.g. + - `s` for `string` + - `i` for signed integer types + - `u` for unsigned integer types + - `x` for `bool` + - `f` for `float`, `double`, or `decimal` +- Prefer meaningful domain names over type prefixes when the intent is already clear. +- In UI code, use short 3-character prefixes for actual controls in views and code-behind, e.g. + - `lst` for list controls + - `btn` for all kind of buttons, + - `edt` for any keyboard input control + - `lbl` for any text output control +- Do not use UI control prefixes for ViewModel properties or other non-UI members. + +## Nullability +- Use strict nullable reference types to indicate when a variable can be null, and handle nullability appropriately in code. diff --git a/TestStatements/.github/upgrades/dotnet-upgrade-plan.md b/TestStatements/.github/upgrades/dotnet-upgrade-plan.md new file mode 100644 index 000000000..c4a8a1fd8 --- /dev/null +++ b/TestStatements/.github/upgrades/dotnet-upgrade-plan.md @@ -0,0 +1,33 @@ +# .NET 8.0 Upgrade Plan + +## Execution Steps + +Execute steps below sequentially one by one in the order they are listed. + +1. Validate that an .NET 8.0 SDK required for this upgrade is installed on the machine and if not, help to get it installed. +2. Ensure that the SDK version specified in global.json files is compatible with the .NET 8.0 upgrade. +3. Upgrade UWP_00_Test\UWP_00_Test.csproj + +## Settings + +This section contains settings and data used by execution steps. + +### Excluded projects + +Table below contains projects that do belong to the dependency graph for selected projects and should not be included in the upgrade. + +| Project name | Description | +|:-------------|:-----------:| + +### Project upgrade details +This section contains details about each project upgrade and modifications that need to be done in the project. + +#### UWP_00_Test\UWP_00_Test.csproj modifications + +Project properties changes: + - Include shared props: `..\MVVM_Tutorial.props` + - Enable nullable reference types: `nullable` set to `enable` + - Migrate project to SDK style targeting .NET 8 (Windows): adopt `Microsoft.NET.Sdk` with appropriate target framework if required by the app model + +Other changes: + - Review and adjust Windows application model dependencies as needed for .NET 8 (e.g., Windows App SDK / WinUI migration if applicable) From 4209a505f0e2906aea68cf96a30380a0ca067178 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:10:20 +0200 Subject: [PATCH 75/96] AppWithPluginTest --- TestStatements/AppWithPluginTest/AppWithPluginTest.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TestStatements/AppWithPluginTest/AppWithPluginTest.csproj b/TestStatements/AppWithPluginTest/AppWithPluginTest.csproj index 3cddb82ae..86c563215 100644 --- a/TestStatements/AppWithPluginTest/AppWithPluginTest.csproj +++ b/TestStatements/AppWithPluginTest/AppWithPluginTest.csproj @@ -14,8 +14,8 @@ - - + + From d54e4185df291827ee93860ba6c58fe31a89e33b Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:10:28 +0200 Subject: [PATCH 76/96] HelloPluginTest --- TestStatements/HelloPluginTest/HelloPluginTest.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TestStatements/HelloPluginTest/HelloPluginTest.csproj b/TestStatements/HelloPluginTest/HelloPluginTest.csproj index 5d37750ef..955149e92 100644 --- a/TestStatements/HelloPluginTest/HelloPluginTest.csproj +++ b/TestStatements/HelloPluginTest/HelloPluginTest.csproj @@ -13,8 +13,8 @@ - - + + From 9f1c5f07c0050a715583c31dc178f68d53ebe25a Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:10:33 +0200 Subject: [PATCH 77/96] TestGJKAlgTest --- TestStatements/TestGJKAlgTest/TestGJKAlgTest.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TestStatements/TestGJKAlgTest/TestGJKAlgTest.csproj b/TestStatements/TestGJKAlgTest/TestGJKAlgTest.csproj index 0e08213c1..f70e52a30 100644 --- a/TestStatements/TestGJKAlgTest/TestGJKAlgTest.csproj +++ b/TestStatements/TestGJKAlgTest/TestGJKAlgTest.csproj @@ -8,8 +8,8 @@ - - + + From 17831b91b9b2fd00474b4da971a9c6c441d38053 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:10:39 +0200 Subject: [PATCH 78/96] TestStatementsTest --- TestStatements/TestStatementsTest/TestStatementsTest.csproj | 4 ++-- .../TestStatementsTest/TestStatements_netTest.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/TestStatements/TestStatementsTest/TestStatementsTest.csproj b/TestStatements/TestStatementsTest/TestStatementsTest.csproj index a7c4458c0..32f2346bc 100644 --- a/TestStatements/TestStatementsTest/TestStatementsTest.csproj +++ b/TestStatements/TestStatementsTest/TestStatementsTest.csproj @@ -31,8 +31,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/TestStatements/TestStatementsTest/TestStatements_netTest.csproj b/TestStatements/TestStatementsTest/TestStatements_netTest.csproj index e489d2227..fca4f1508 100644 --- a/TestStatements/TestStatementsTest/TestStatements_netTest.csproj +++ b/TestStatements/TestStatementsTest/TestStatements_netTest.csproj @@ -32,8 +32,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive From 2ed5180df65a06dd46e376d016ce125703c67f52 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:11:37 +0200 Subject: [PATCH 79/96] Analyzer1 --- Transpiler_pp/.github/copilot-instructions.md | 48 +++++++++++++++++++ .../.github/upgrades/dotnet-upgrade-plan.md | 33 +++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 Transpiler_pp/.github/copilot-instructions.md create mode 100644 Transpiler_pp/.github/upgrades/dotnet-upgrade-plan.md diff --git a/Transpiler_pp/.github/copilot-instructions.md b/Transpiler_pp/.github/copilot-instructions.md new file mode 100644 index 000000000..77988b00d --- /dev/null +++ b/Transpiler_pp/.github/copilot-instructions.md @@ -0,0 +1,48 @@ +# Repository instructions for GitHub Copilot + +Apply these defaults when working in this repository unless the user explicitly asks otherwise: + +## General Guidelines +- Document code thoroughly in English. +- Validate changes with relevant builds and tests before finishing. +- If requirements are unclear, ask clarifying questions before starting implementation or planning refinement. +- Avoid UI text strings in core services. Use Enumerations instead, and keep UI-facing strings in the ViewModel/UI layer. +- Prefer one class/interface/struct per file. +- document changes in an DevOps-manner markdown prefered, extrapolate bugs, tasks, baglogs and features +- Use `DevOps` as the planning directory in this workspace, and treat `.Info.md` as the general planning description file. Team terminology around Azure DevOps backlog items may differ from generic 'story' naming. + +## Testing +- Use `MSTest` in the latest practical version for new or updated tests. +- Use `NSubstitute` for mocks, stubs, and substitutes in tests. +- Prefer `DataRow` for parameterized single-test scenarios. + +## Internationalization +- Keep I18N in mind when writing code, ensuring it can be easily adapted for different languages and regions. + +## Architecture +- Use MVVM architecture for UI components to separate concerns and improve testability, using CommunityToolkit.Mvvm for MVVM implementation. +- Prefer `NotifyPropertyChangedFor` over manual `OnPropertyChanged(nameof(...))` in CommunityToolkit.Mvvm observable properties where applicable. +- Use Dependency Injection to manage dependencies and improve testability, using Microsoft.Extensions.DependencyInjection. +- UI-facing strings and summary formatting should stay in the ViewModel/UI layer, not in extracted application logic services. + +## Naming Conventions +- Distinguish between UI control naming and variable/field naming. +- Use PascalCase for class names, method names, and properties. +- Use `_camelCase` for private fields. +- Use `camelCase` for local variables and parameters. +- Use short 1-character prefixes for simple types only when they meaningfully disambiguate, e.g. + - `s` for `string` + - `i` for signed integer types + - `u` for unsigned integer types + - `x` for `bool` + - `f` for `float`, `double`, or `decimal` +- Prefer meaningful domain names over type prefixes when the intent is already clear. +- In UI code, use short 3-character prefixes for actual controls in views and code-behind, e.g. + - `lst` for list controls + - `btn` for all kind of buttons, + - `edt` for any keyboard input control + - `lbl` for any text output control +- Do not use UI control prefixes for ViewModel properties or other non-UI members. + +## Nullability +- Use strict nullable reference types to indicate when a variable can be null, and handle nullability appropriately in code. diff --git a/Transpiler_pp/.github/upgrades/dotnet-upgrade-plan.md b/Transpiler_pp/.github/upgrades/dotnet-upgrade-plan.md new file mode 100644 index 000000000..c4a8a1fd8 --- /dev/null +++ b/Transpiler_pp/.github/upgrades/dotnet-upgrade-plan.md @@ -0,0 +1,33 @@ +# .NET 8.0 Upgrade Plan + +## Execution Steps + +Execute steps below sequentially one by one in the order they are listed. + +1. Validate that an .NET 8.0 SDK required for this upgrade is installed on the machine and if not, help to get it installed. +2. Ensure that the SDK version specified in global.json files is compatible with the .NET 8.0 upgrade. +3. Upgrade UWP_00_Test\UWP_00_Test.csproj + +## Settings + +This section contains settings and data used by execution steps. + +### Excluded projects + +Table below contains projects that do belong to the dependency graph for selected projects and should not be included in the upgrade. + +| Project name | Description | +|:-------------|:-----------:| + +### Project upgrade details +This section contains details about each project upgrade and modifications that need to be done in the project. + +#### UWP_00_Test\UWP_00_Test.csproj modifications + +Project properties changes: + - Include shared props: `..\MVVM_Tutorial.props` + - Enable nullable reference types: `nullable` set to `enable` + - Migrate project to SDK style targeting .NET 8 (Windows): adopt `Microsoft.NET.Sdk` with appropriate target framework if required by the app model + +Other changes: + - Review and adjust Windows application model dependencies as needed for .NET 8 (e.g., Windows App SDK / WinUI migration if applicable) From 42e55dc3d649187ada6464799baa4ba35caff862 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:11:39 +0200 Subject: [PATCH 80/96] Analyzer1.Test --- Transpiler_pp/Analyzer1/Analyzer1.Test/Analyzer1.Test.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Transpiler_pp/Analyzer1/Analyzer1.Test/Analyzer1.Test.csproj b/Transpiler_pp/Analyzer1/Analyzer1.Test/Analyzer1.Test.csproj index 4a3382906..c9aa8d937 100644 --- a/Transpiler_pp/Analyzer1/Analyzer1.Test/Analyzer1.Test.csproj +++ b/Transpiler_pp/Analyzer1/Analyzer1.Test/Analyzer1.Test.csproj @@ -8,8 +8,8 @@ - - + + From b94ce50ccb16f5ee4d4a9c23ca4c1287a71e2da1 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:11:39 +0200 Subject: [PATCH 81/96] Analyzer1.Vsix --- Transpiler_pp/Analyzer1/Analyzer1.Vsix/Analyzer1.Vsix.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Transpiler_pp/Analyzer1/Analyzer1.Vsix/Analyzer1.Vsix.csproj b/Transpiler_pp/Analyzer1/Analyzer1.Vsix/Analyzer1.Vsix.csproj index 94f25de42..fe9a470d5 100644 --- a/Transpiler_pp/Analyzer1/Analyzer1.Vsix/Analyzer1.Vsix.csproj +++ b/Transpiler_pp/Analyzer1/Analyzer1.Vsix/Analyzer1.Vsix.csproj @@ -19,7 +19,7 @@ - + From fa2f0a3deacfd2c691ec0cb200bdfda7b078758e Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:11:41 +0200 Subject: [PATCH 82/96] TranspilerLib.CSharp.Tests --- .../TranspilerLib.CSharp.Tests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Transpiler_pp/TranspilerLib.CSharp.Tests/TranspilerLib.CSharp.Tests.csproj b/Transpiler_pp/TranspilerLib.CSharp.Tests/TranspilerLib.CSharp.Tests.csproj index 5718b8ebc..2b930376a 100644 --- a/Transpiler_pp/TranspilerLib.CSharp.Tests/TranspilerLib.CSharp.Tests.csproj +++ b/Transpiler_pp/TranspilerLib.CSharp.Tests/TranspilerLib.CSharp.Tests.csproj @@ -24,8 +24,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + From cc4c499d0f0e0da8a58cf05fc21ea6ad60278da8 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:11:42 +0200 Subject: [PATCH 83/96] TranspilerLib.DriveBASIC.Tests --- .../TranspilerLib.DriveBASIC.Tests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Transpiler_pp/TranspilerLib.DriveBASIC.Tests/TranspilerLib.DriveBASIC.Tests.csproj b/Transpiler_pp/TranspilerLib.DriveBASIC.Tests/TranspilerLib.DriveBASIC.Tests.csproj index 58b56ead6..1dc0b4a94 100644 --- a/Transpiler_pp/TranspilerLib.DriveBASIC.Tests/TranspilerLib.DriveBASIC.Tests.csproj +++ b/Transpiler_pp/TranspilerLib.DriveBASIC.Tests/TranspilerLib.DriveBASIC.Tests.csproj @@ -24,8 +24,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + From 9994640f32b0a9e31491dd3633be48a4600f9e8f Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:11:43 +0200 Subject: [PATCH 84/96] TranspilerLib.IEC.Tests --- .../TranspilerLib.IEC.Tests/TranspilerLib.IEC.Tests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Transpiler_pp/TranspilerLib.IEC.Tests/TranspilerLib.IEC.Tests.csproj b/Transpiler_pp/TranspilerLib.IEC.Tests/TranspilerLib.IEC.Tests.csproj index c2f9b939b..d78ccb65c 100644 --- a/Transpiler_pp/TranspilerLib.IEC.Tests/TranspilerLib.IEC.Tests.csproj +++ b/Transpiler_pp/TranspilerLib.IEC.Tests/TranspilerLib.IEC.Tests.csproj @@ -24,8 +24,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + From 22cbb8349520eab055ed016b9d0d62143b108646 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:11:44 +0200 Subject: [PATCH 85/96] TranspilerLib.Pascal.Tests --- .../TranspilerLib.Pascal.Tests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Transpiler_pp/TranspilerLib.Pascal.Tests/TranspilerLib.Pascal.Tests.csproj b/Transpiler_pp/TranspilerLib.Pascal.Tests/TranspilerLib.Pascal.Tests.csproj index 3af65ca10..f9907da3b 100644 --- a/Transpiler_pp/TranspilerLib.Pascal.Tests/TranspilerLib.Pascal.Tests.csproj +++ b/Transpiler_pp/TranspilerLib.Pascal.Tests/TranspilerLib.Pascal.Tests.csproj @@ -24,8 +24,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + From 5c4d81612fdc3adf27dc2b52acc792e3c2afd78b Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:11:44 +0200 Subject: [PATCH 86/96] TranspilerLibTests --- Transpiler_pp/TranspilerLibTests/TranspilerLibTests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Transpiler_pp/TranspilerLibTests/TranspilerLibTests.csproj b/Transpiler_pp/TranspilerLibTests/TranspilerLibTests.csproj index ec02c718a..3b9703b06 100644 --- a/Transpiler_pp/TranspilerLibTests/TranspilerLibTests.csproj +++ b/Transpiler_pp/TranspilerLibTests/TranspilerLibTests.csproj @@ -37,8 +37,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + From 535496ea00459d2935ac1b9fa7c9908b9eaa4b50 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:11:45 +0200 Subject: [PATCH 87/96] Trnsp.Show.Lfm.Tests --- .../Trnsp.Show.Lfm.Tests/Trnsp.Show.Lfm.Tests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Transpiler_pp/Trnsp.Show.Lfm.Tests/Trnsp.Show.Lfm.Tests.csproj b/Transpiler_pp/Trnsp.Show.Lfm.Tests/Trnsp.Show.Lfm.Tests.csproj index 471bc8d3f..58875cbcf 100644 --- a/Transpiler_pp/Trnsp.Show.Lfm.Tests/Trnsp.Show.Lfm.Tests.csproj +++ b/Transpiler_pp/Trnsp.Show.Lfm.Tests/Trnsp.Show.Lfm.Tests.csproj @@ -15,8 +15,8 @@ $(TargetFrameworks);net10.0-windows - - + + From 3e7858f82fb095029241951787d959033b3ad1d1 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:11:46 +0200 Subject: [PATCH 88/96] Trnsp.Show.Pas.Tests --- .../Trnsp.Show.Pas.Tests/Trnsp.Show.Pas.Tests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Transpiler_pp/Trnsp.Show.Pas.Tests/Trnsp.Show.Pas.Tests.csproj b/Transpiler_pp/Trnsp.Show.Pas.Tests/Trnsp.Show.Pas.Tests.csproj index ab421e875..a1277832b 100644 --- a/Transpiler_pp/Trnsp.Show.Pas.Tests/Trnsp.Show.Pas.Tests.csproj +++ b/Transpiler_pp/Trnsp.Show.Pas.Tests/Trnsp.Show.Pas.Tests.csproj @@ -15,8 +15,8 @@ $(TargetFrameworks);net10.0-windows - - + + From 54a70b08bce354505e82900fd1a25f6075abf13d Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:19:40 +0200 Subject: [PATCH 89/96] AA05_CommandParCalc --- .../.github/upgrades/dotnet-upgrade-plan.md | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 Avalonia_Apps/.github/upgrades/dotnet-upgrade-plan.md diff --git a/Avalonia_Apps/.github/upgrades/dotnet-upgrade-plan.md b/Avalonia_Apps/.github/upgrades/dotnet-upgrade-plan.md new file mode 100644 index 000000000..c4a8a1fd8 --- /dev/null +++ b/Avalonia_Apps/.github/upgrades/dotnet-upgrade-plan.md @@ -0,0 +1,33 @@ +# .NET 8.0 Upgrade Plan + +## Execution Steps + +Execute steps below sequentially one by one in the order they are listed. + +1. Validate that an .NET 8.0 SDK required for this upgrade is installed on the machine and if not, help to get it installed. +2. Ensure that the SDK version specified in global.json files is compatible with the .NET 8.0 upgrade. +3. Upgrade UWP_00_Test\UWP_00_Test.csproj + +## Settings + +This section contains settings and data used by execution steps. + +### Excluded projects + +Table below contains projects that do belong to the dependency graph for selected projects and should not be included in the upgrade. + +| Project name | Description | +|:-------------|:-----------:| + +### Project upgrade details +This section contains details about each project upgrade and modifications that need to be done in the project. + +#### UWP_00_Test\UWP_00_Test.csproj modifications + +Project properties changes: + - Include shared props: `..\MVVM_Tutorial.props` + - Enable nullable reference types: `nullable` set to `enable` + - Migrate project to SDK style targeting .NET 8 (Windows): adopt `Microsoft.NET.Sdk` with appropriate target framework if required by the app model + +Other changes: + - Review and adjust Windows application model dependencies as needed for .NET 8 (e.g., Windows App SDK / WinUI migration if applicable) From ca7a14c90b465d3db479722417d1e4a9f40bfe2f Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:19:43 +0200 Subject: [PATCH 90/96] AA09_DialogBoxes --- .../AA09_DialogBoxes/AA09_DialogBoxes.csproj | 4 ++- .../Messages/MessageBoxRequestMessage.cs | 8 +++--- .../ViewModels/DialogViewModel.cs | 3 +-- .../ViewModels/MainWindowViewModel.cs | 18 ++++++++----- .../AA09_DialogBoxes/Views/MainWindow.axaml | 22 +++++++++------- .../Views/MainWindow.axaml.cs | 26 +++++++++++++------ 6 files changed, 51 insertions(+), 30 deletions(-) diff --git a/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/AA09_DialogBoxes.csproj b/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/AA09_DialogBoxes.csproj index 5b75d64e7..b4bf5c09b 100644 --- a/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/AA09_DialogBoxes.csproj +++ b/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/AA09_DialogBoxes.csproj @@ -34,7 +34,6 @@ All - @@ -57,6 +56,9 @@ DialogWindow.axaml + + OverlayDialogControl.axaml + App.axaml diff --git a/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Messages/MessageBoxRequestMessage.cs b/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Messages/MessageBoxRequestMessage.cs index fe86a4a9f..9b945546d 100644 --- a/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Messages/MessageBoxRequestMessage.cs +++ b/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Messages/MessageBoxRequestMessage.cs @@ -1,14 +1,14 @@ -using System.Threading.Tasks; using CommunityToolkit.Mvvm.Messaging.Messages; -using AA09_DialogBoxes.ViewModels; -using MsBox.Avalonia.Enums; namespace AA09_DialogBoxes.Messages; -public sealed class MessageBoxRequestMessage : AsyncRequestMessage +public enum MsgBoxResult { None, Yes, No } + +public sealed class MessageBoxRequestMessage : AsyncRequestMessage { public string Title { get; } public string Content { get; } + public MessageBoxRequestMessage(string title, string content) { Title = title; diff --git a/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/ViewModels/DialogViewModel.cs b/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/ViewModels/DialogViewModel.cs index 5799a1c1a..a3bf5a8b6 100644 --- a/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/ViewModels/DialogViewModel.cs +++ b/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/ViewModels/DialogViewModel.cs @@ -17,7 +17,6 @@ using CommunityToolkit.Mvvm.Messaging; using AA09_DialogBoxes.Messages; using System.Threading.Tasks; -using MsBox.Avalonia.Enums; namespace AA09_DialogBoxes.ViewModels; @@ -55,7 +54,7 @@ private async Task OpenMsg() var request = new MessageBoxRequestMessage("Frage", "Willst Du Das ?"); WeakReferenceMessenger.Default.Send(request); var result = await request.Response; - Name = result == ButtonResult.Yes ? "42 Entwickler" : "Nö"; + Name = result == MsgBoxResult.Yes ? "42 Entwickler" : "Nö"; } [RelayCommand] diff --git a/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/ViewModels/MainWindowViewModel.cs b/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/ViewModels/MainWindowViewModel.cs index 65a105d35..1b76b75cb 100644 --- a/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/ViewModels/MainWindowViewModel.cs +++ b/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/ViewModels/MainWindowViewModel.cs @@ -17,19 +17,16 @@ using AA09_DialogBoxes.Messages; using Avalonia.ViewModels; using System.Threading.Tasks; -using MsBox.Avalonia.Enums; namespace AA09_DialogBoxes.ViewModels; -public enum MsgBoxResult { None, Yes, No } - public partial class MainWindowViewModel : ViewModelBase { #region Properties [ObservableProperty] - public partial string Name { get; set; }= ""; + public partial string Name { get; set;} = ""; [ObservableProperty] - public partial string Email { get; set; }= ""; + public partial string Email { get; set; } = ""; [ObservableProperty] private int cnt = 1; #endregion @@ -42,12 +39,21 @@ private async Task OpenMsg() var request = new MessageBoxRequestMessage("Frage", "Willst Du Das ?"); WeakReferenceMessenger.Default.Send(request); var result = await request.Response; - if (result == ButtonResult.Yes) + if (result == MsgBoxResult.Yes) Name = "42 Entwickler"; else Name = "Nö"; } + [RelayCommand] + private async Task OpenOverlayMsg() + { + var request = new OverlayMessageRequestMessage("Overlay Frage", "Soll der In-Window-Overlay-Dialog verwendet werden?"); + WeakReferenceMessenger.Default.Send(request); + var result = await request.Response; + Name = result == MsgBoxResult.Yes ? "Overlay: Ja" : "Overlay: Nein"; + } + [RelayCommand] private async Task OpenDialog() { diff --git a/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Views/MainWindow.axaml b/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Views/MainWindow.axaml index 9481d6187..4d6afbc2b 100644 --- a/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Views/MainWindow.axaml +++ b/Avalonia_Apps/AA09_DialogBoxes/AA09_DialogBoxes/Views/MainWindow.axaml @@ -6,18 +6,22 @@ x:Class="AA09_DialogBoxes.Views.MainWindow" Width="600" Height="400" Title="Dialog Demo" - x:DataType="vm:MainWindowViewModel" + x:DataType="vm:MainWindowViewModel" mc:Ignorable="d"> - - - - -