From caabb8af0de79cdd9f959f219850e7e975850d7d Mon Sep 17 00:00:00 2001 From: Joe Care Date: Thu, 9 Apr 2026 18:23:32 +0200 Subject: [PATCH 01/90] MVVM_Tutorial --- .../WpfApp1/Models/TextDocumentModel.cs | 21 ++++++++++ .../WpfApp1/Services/FileDialogService.cs | 39 +++++++++++++++++ .../WpfApp1/Services/IFileDialogService.cs | 18 ++++++++ .../WpfApp1/Services/ITextDocumentService.cs | 25 +++++++++++ .../WpfApp1/Services/TextDocumentService.cs | 42 +++++++++++++++++++ 5 files changed, 145 insertions(+) create mode 100644 CSharpBible/MVVM_Tutorial/WpfApp1/Models/TextDocumentModel.cs create mode 100644 CSharpBible/MVVM_Tutorial/WpfApp1/Services/FileDialogService.cs create mode 100644 CSharpBible/MVVM_Tutorial/WpfApp1/Services/IFileDialogService.cs create mode 100644 CSharpBible/MVVM_Tutorial/WpfApp1/Services/ITextDocumentService.cs create mode 100644 CSharpBible/MVVM_Tutorial/WpfApp1/Services/TextDocumentService.cs diff --git a/CSharpBible/MVVM_Tutorial/WpfApp1/Models/TextDocumentModel.cs b/CSharpBible/MVVM_Tutorial/WpfApp1/Models/TextDocumentModel.cs new file mode 100644 index 000000000..014c28df0 --- /dev/null +++ b/CSharpBible/MVVM_Tutorial/WpfApp1/Models/TextDocumentModel.cs @@ -0,0 +1,21 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace WpfApp.Models; + +/// +/// Represents a text document that can be loaded from or saved to a file. +/// +public partial class TextDocumentModel : ObservableObject +{ + /// + /// Gets or sets the file path associated with the document. + /// + [ObservableProperty] + private string? _filePath; + + /// + /// Gets or sets the document text. + /// + [ObservableProperty] + private string _text = string.Empty; +} diff --git a/CSharpBible/MVVM_Tutorial/WpfApp1/Services/FileDialogService.cs b/CSharpBible/MVVM_Tutorial/WpfApp1/Services/FileDialogService.cs new file mode 100644 index 000000000..2daeafd44 --- /dev/null +++ b/CSharpBible/MVVM_Tutorial/WpfApp1/Services/FileDialogService.cs @@ -0,0 +1,39 @@ +using System.IO; +using Microsoft.Win32; + +namespace WpfApp.Services; + +/// +/// Implements WPF file dialogs for text document operations. +/// +public sealed class FileDialogService : IFileDialogService +{ + /// + public string? PickOpenFilePath() + { + var oDialog = new OpenFileDialog + { + Filter = "Text files (*.txt)|*.txt|All files (*.*)|*.*", + Multiselect = false + }; + + return oDialog.ShowDialog() == true ? oDialog.FileName : null; + } + + /// + public string? PickSaveFilePath(string? initialFilePath) + { + var oDialog = new SaveFileDialog + { + Filter = "Text files (*.txt)|*.txt|All files (*.*)|*.*", + FileName = string.IsNullOrWhiteSpace(initialFilePath) ? "Document.txt" : Path.GetFileName(initialFilePath) + }; + + if (!string.IsNullOrWhiteSpace(initialFilePath)) + { + oDialog.InitialDirectory = Path.GetDirectoryName(initialFilePath); + } + + return oDialog.ShowDialog() == true ? oDialog.FileName : null; + } +} diff --git a/CSharpBible/MVVM_Tutorial/WpfApp1/Services/IFileDialogService.cs b/CSharpBible/MVVM_Tutorial/WpfApp1/Services/IFileDialogService.cs new file mode 100644 index 000000000..7a8494c6b --- /dev/null +++ b/CSharpBible/MVVM_Tutorial/WpfApp1/Services/IFileDialogService.cs @@ -0,0 +1,18 @@ +namespace WpfApp.Services; + +/// +/// Provides file dialog interaction for opening and saving text documents. +/// +public interface IFileDialogService +{ + /// + /// Shows the open-file dialog and returns the selected file path. + /// + string? PickOpenFilePath(); + + /// + /// Shows the save-file dialog and returns the selected file path. + /// + /// The initial file path, if any. + string? PickSaveFilePath(string? initialFilePath); +} diff --git a/CSharpBible/MVVM_Tutorial/WpfApp1/Services/ITextDocumentService.cs b/CSharpBible/MVVM_Tutorial/WpfApp1/Services/ITextDocumentService.cs new file mode 100644 index 000000000..14d2a7c86 --- /dev/null +++ b/CSharpBible/MVVM_Tutorial/WpfApp1/Services/ITextDocumentService.cs @@ -0,0 +1,25 @@ +using System.Threading; +using System.Threading.Tasks; +using WpfApp.Models; + +namespace WpfApp.Services; + +/// +/// Provides file-based load and save operations for text documents. +/// +public interface ITextDocumentService +{ + /// + /// Loads a text document from the specified file path. + /// + /// The file path to load. + /// The cancellation token. + /// The loaded document. + Task LoadAsync(string filePath, CancellationToken cancellationToken = default); + + /// + /// Saves a text document to its associated file path. + /// The document to save. + /// The cancellation token. + Task SaveAsync(TextDocumentModel document, CancellationToken cancellationToken = default); +} diff --git a/CSharpBible/MVVM_Tutorial/WpfApp1/Services/TextDocumentService.cs b/CSharpBible/MVVM_Tutorial/WpfApp1/Services/TextDocumentService.cs new file mode 100644 index 000000000..e901c540e --- /dev/null +++ b/CSharpBible/MVVM_Tutorial/WpfApp1/Services/TextDocumentService.cs @@ -0,0 +1,42 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using WpfApp.Models; + +namespace WpfApp.Services; + +/// +/// Implements file-based text document storage. +/// +public sealed class TextDocumentService : ITextDocumentService +{ + /// + public async Task LoadAsync(string filePath, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(filePath); + + var text = await File.ReadAllTextAsync(filePath, cancellationToken).ConfigureAwait(false); + return new TextDocumentModel + { + FilePath = filePath, + Text = text + }; + } + + /// + public async Task SaveAsync(TextDocumentModel document, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(document); + ArgumentException.ThrowIfNullOrWhiteSpace(document.FilePath); + + var sFilePath = document.FilePath; + var sDirectory = Path.GetDirectoryName(sFilePath); + if (!string.IsNullOrWhiteSpace(sDirectory)) + { + Directory.CreateDirectory(sDirectory); + } + + await File.WriteAllTextAsync(sFilePath, document.Text ?? string.Empty, cancellationToken).ConfigureAwait(false); + } +} From 4bc3951364441a96914b664ebffca5d909d119f4 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Thu, 9 Apr 2026 19:00:46 +0200 Subject: [PATCH 02/90] 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 03/90] 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 04/90] 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 05/90] 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 06/90] 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 07/90] 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 bb99457cd21d816175a93c8ef78c25ff972c3c03 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Fri, 10 Apr 2026 00:32:42 +0200 Subject: [PATCH 08/90] MVVM_Tutorial --- CSharpBible/MVVM_Tutorial/WpfApp1/WpfApp_net.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 09/90] 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 10/90] 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 11/90] 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 12/90] 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 13/90] 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 14/90] 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 15/90] 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 16/90] 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 17/90] 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 18/90] 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 19/90] 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 20/90] 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 21/90] 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 22/90] 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 23/90] 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 24/90] 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 25/90] 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 26/90] 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 27/90] 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 28/90] 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 29/90] 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 30/90] 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 31/90] 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 32/90] 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 33/90] 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 34/90] 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 35/90] 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 36/90] 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 37/90] 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 38/90] 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 39/90] 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 40/90] 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 41/90] 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 42/90] 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 43/90] 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 44/90] 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 45/90] 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 46/90] 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 47/90] 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 48/90] 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 49/90] 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 50/90] 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 51/90] 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 52/90] 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 53/90] 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 54/90] 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 55/90] 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 56/90] 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 57/90] 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 58/90] 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 60/90] 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 61/90] 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 62/90] 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 63/90] 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 64/90] 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 65/90] 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 9517578c4b9d85346007b8548fe9fbe584e31f06 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:06:48 +0200 Subject: [PATCH 66/90] 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 67/90] 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 68/90] 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 69/90] 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 2b33c4abc5c3c9ecfdd87ef610abdffa898336d8 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:08:57 +0200 Subject: [PATCH 70/90] MVVM_Tutorial --- .../.github/copilot-instructions.md | 48 +++++++++++++++++++ .../.github/upgrades/dotnet-upgrade-plan.md | 33 +++++++++++++ .../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 +- 105 files changed, 287 insertions(+), 206 deletions(-) create mode 100644 CSharpBible/MVVM_Tutorial/.github/copilot-instructions.md create mode 100644 CSharpBible/MVVM_Tutorial/.github/upgrades/dotnet-upgrade-plan.md diff --git a/CSharpBible/MVVM_Tutorial/.github/copilot-instructions.md b/CSharpBible/MVVM_Tutorial/.github/copilot-instructions.md new file mode 100644 index 000000000..77988b00d --- /dev/null +++ b/CSharpBible/MVVM_Tutorial/.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/CSharpBible/MVVM_Tutorial/.github/upgrades/dotnet-upgrade-plan.md b/CSharpBible/MVVM_Tutorial/.github/upgrades/dotnet-upgrade-plan.md new file mode 100644 index 000000000..c4a8a1fd8 --- /dev/null +++ b/CSharpBible/MVVM_Tutorial/.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) 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 From 36dbac49c8357f236001f61bf7a4dbda3dca1052 Mon Sep 17 00:00:00 2001 From: Joe Care Date: Sun, 12 Apr 2026 19:10:20 +0200 Subject: [PATCH 71/90] 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 72/90] 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 73/90] 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 74/90] 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 75/90] 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 76/90] 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 77/90] 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 78/90] 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 79/90] 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 80/90] 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 81/90] 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 82/90] 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 83/90] 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 84/90] 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 85/90] 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 86/90] 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 87/90] 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"> - - - - -