diff --git a/README.md b/README.md
index 5ae388a0..618b889c 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,9 @@
# LogExpert [](https://github.com/LogExperts/LogExpert/actions/workflows/build_dotnet.yml)
-This is a clone from (no longer exists) https://logexpert.codeplex.com/
+This is a clone from (no longer exists)
+
+## Overview
-# Overview
LogExpert is a Windows tail program (a GUI replacement for the Unix tail command).
Summary of (most) features:
@@ -23,7 +24,8 @@ Summary of (most) features:
* Serilog.Formatting.Compact format support (Experimental)
* Portable (all options / settings saved in application startup directory)
-# Download
+## Download
+
Follow the [Link](https://github.com/LogExperts/LogExpert/releases/latest) and download the latest package. Just extract it where you want and execute the application or download the Setup and install it
Or Install via chocolatey
@@ -31,43 +33,52 @@ Or Install via chocolatey
```choco install logexpert```
Requirements
-- https://dotnet.microsoft.com/en-us/download/dotnet/8.0
-- .NET 8 (https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.13-windows-x64-installer or https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.13-windows-x86-installer)
+
+*
+* .NET 8 ( or )
## CI
+
This is a continous integration build. So always the latest and greates changes. It should be stable but no promises. Can be viewed as Beta.
[CI Download](https://ci.appveyor.com/project/Zarunbal/logexpert)
-# How to Build
+## How to Build
-- Clone / Fork / Download the source code
-- Open the Solution (src/LogExpert.sln) with Visual Studio 2017 (e.g. Community Edition)
-- Restore Nuget Packages on Solution
-- Build
-- The output is under bin/(Debug/Release)/
+* Clone / Fork / Download the source code
+* Open the Solution (src/LogExpert.sln) with Visual Studio 2017 (e.g. Community Edition)
+* Restore Nuget Packages on Solution
+* Build
+* The output is under bin/(Debug/Release)/
Nuke.build Requirements
-- Chocolatey must be installed
-- Optional for Setup Inno Script 5 or 6
-# Pull Request
-- Use Development branch as target
+* Chocolatey must be installed
+* Optional for Setup Inno Script 5 or 6
+
+## Pull Request
+
+* Use Development branch as target
+
+## FAQ / HELP / Informations / Examples
-# FAQ / HELP / Informations / Examples
Please checkout the wiki for FAQ / HELP / Informations / Examples
-# High DPI
-- dont use AutoScaleMode for single GUI controls like Buttons etc.
-- dont use AutoScaleDimensions for single GUI controls like Buttons etc.
+## High DPI
+
+* dont use AutoScaleMode for single GUI controls like Buttons etc.
+
+* dont use AutoScaleDimensions for single GUI controls like Buttons etc.
+
+
+
+## Discord Server
-https://github.com/LogExperts/LogExpert/wiki
+
-# Discord Server
-https://discord.gg/SjxkuckRe9
+### Credits
-## Credits
-### Contributors
+#### Contributors
This project exists thanks to all the people who contribute.
diff --git a/src/.editorconfig b/src/.editorconfig
index 0792babf..af6a16f3 100644
--- a/src/.editorconfig
+++ b/src/.editorconfig
@@ -34,23 +34,23 @@ dotnet_sort_system_directives_first = true
file_header_template = unset
# this. and Me. preferences
-dotnet_style_qualification_for_event = false
-dotnet_style_qualification_for_field = false
-dotnet_style_qualification_for_method = false
-dotnet_style_qualification_for_property = false
+dotnet_style_qualification_for_event = false:suggestion
+dotnet_style_qualification_for_field = false:suggestion
+dotnet_style_qualification_for_method = false:suggestion
+dotnet_style_qualification_for_property = false:suggestion
# Language keywords vs BCL types preferences
-dotnet_style_predefined_type_for_locals_parameters_members = true:warning
-dotnet_style_predefined_type_for_member_access = true:warning
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+dotnet_style_predefined_type_for_member_access = true:suggestion
# Parentheses preferences
-dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning
+dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning
-dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning
+dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none
# Modifier preferences
-dotnet_style_require_accessibility_modifiers = for_non_interface_members
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
# Expression-level preferences
dotnet_prefer_system_hash_code = true
@@ -89,22 +89,22 @@ dotnet_style_allow_statement_immediately_after_block_experimental = false:warnin
#### C# Coding Conventions ####
# var preferences
-csharp_style_var_elsewhere = true:suggestion
-csharp_style_var_for_built_in_types = true:warning
-csharp_style_var_when_type_is_apparent = true:warning
+csharp_style_var_elsewhere = false:suggestion
+csharp_style_var_for_built_in_types = false:suggestion
+csharp_style_var_when_type_is_apparent = true:suggestion
# Expression-bodied members
csharp_style_expression_bodied_accessors = true:suggestion
-csharp_style_expression_bodied_constructors = false:warning
+csharp_style_expression_bodied_constructors = false:suggestion
csharp_style_expression_bodied_indexers = true:suggestion
csharp_style_expression_bodied_lambdas = true:suggestion
csharp_style_expression_bodied_local_functions = true:suggestion
-csharp_style_expression_bodied_methods = true:suggestion
+csharp_style_expression_bodied_methods = false:suggestion
csharp_style_expression_bodied_operators = true:suggestion
csharp_style_expression_bodied_properties = true:suggestion
# Pattern matching preferences
-csharp_style_pattern_matching_over_as_with_null_check = true:warning
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_pattern_matching_over_is_with_cast_check = true:warning
csharp_style_prefer_extended_property_pattern = true:warning
csharp_style_prefer_not_pattern = true:warning
@@ -120,7 +120,7 @@ csharp_prefer_static_anonymous_function = true
csharp_prefer_static_local_function = true:warning
csharp_style_prefer_readonly_struct = true
csharp_style_prefer_readonly_struct_member = true
-csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
+csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion
# Code-block preferences
csharp_prefer_braces = true:warning
@@ -132,11 +132,11 @@ csharp_style_prefer_primary_constructors = true:suggestion
csharp_style_prefer_top_level_statements = true:silent
# Expression-level preferences
-csharp_prefer_simple_default_expression = true:warning
+csharp_prefer_simple_default_expression = false:suggestion
csharp_style_deconstructed_variable_declaration = true:warning
csharp_style_implicit_object_creation_when_type_is_apparent = true:warning
-csharp_style_inlined_variable_declaration = true:warning
-csharp_style_prefer_local_over_anonymous_function = true:warning
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_prefer_local_over_anonymous_function = true:suggestion
csharp_style_prefer_index_operator = true:warning
csharp_style_prefer_null_check_over_type_check = true:warning
csharp_style_prefer_range_operator = true:warning
@@ -163,7 +163,7 @@ csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
-csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_object_initializers = false
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
@@ -201,11 +201,61 @@ csharp_space_between_square_brackets = false
# Wrapping preferences
csharp_preserve_single_line_blocks = true
-csharp_preserve_single_line_statements = false
+csharp_preserve_single_line_statements = true
#### Naming styles ####
# Naming rules
+dotnet_naming_rule.constants_rule.import_to_resharper = as_predefined
+dotnet_naming_rule.constants_rule.severity = warning
+dotnet_naming_rule.constants_rule.style = all_upper_style
+dotnet_naming_rule.constants_rule.symbols = constants_symbols
+
+dotnet_naming_rule.local_functions_rule.import_to_resharper = as_predefined
+dotnet_naming_rule.local_functions_rule.severity = warning
+dotnet_naming_rule.local_functions_rule.style = lower_camel_case_style
+dotnet_naming_rule.local_functions_rule.symbols = local_functions_symbols
+
+dotnet_naming_rule.private_constants_rule.import_to_resharper = as_predefined
+dotnet_naming_rule.private_constants_rule.severity = warning
+dotnet_naming_rule.private_constants_rule.style = all_upper_style
+dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols
+
+dotnet_naming_rule.private_static_readonly_rule.import_to_resharper = as_predefined
+dotnet_naming_rule.private_static_readonly_rule.severity = warning
+dotnet_naming_rule.private_static_readonly_rule.style = lower_camel_case_style_1
+dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols
+
+dotnet_naming_rule.type_parameters_rule.import_to_resharper = as_predefined
+dotnet_naming_rule.type_parameters_rule.severity = warning
+dotnet_naming_rule.type_parameters_rule.style = upper_camel_case_style
+dotnet_naming_rule.type_parameters_rule.symbols = type_parameters_symbols
+
+dotnet_naming_style.all_upper_style.capitalization = all_upper
+dotnet_naming_style.all_upper_style.word_separator = _
+
+dotnet_naming_style.lower_camel_case_style.capitalization = camel_case
+dotnet_naming_style.lower_camel_case_style_1.capitalization = camel_case
+dotnet_naming_style.lower_camel_case_style_1.required_prefix = _
+dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case
+
+dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.constants_symbols.applicable_kinds = field
+dotnet_naming_symbols.constants_symbols.required_modifiers = const
+
+dotnet_naming_symbols.local_functions_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.local_functions_symbols.applicable_kinds = local_function
+
+dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private
+dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field
+dotnet_naming_symbols.private_constants_symbols.required_modifiers = const
+
+dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private
+dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field
+dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly
+
+dotnet_naming_symbols.type_parameters_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.type_parameters_symbols.applicable_kinds = type_parameter
dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
@@ -269,6 +319,47 @@ dotnet_diagnostic.CS0649.severity = none
dotnet_diagnostic.CS0169.severity = none
dotnet_diagnostic.CS1591.severity = none
+# ReSharper properties
+resharper_braces_for_for = required
+resharper_braces_for_foreach = required
+resharper_braces_for_ifelse = required
+resharper_braces_for_while = required
+resharper_csharp_align_multiline_parameter = true
+resharper_csharp_insert_final_newline = true
+resharper_csharp_max_line_length = 500
+resharper_csharp_use_indent_from_vs = false
+resharper_csharp_wrap_lines = false
+resharper_indent_nested_fixed_stmt = true
+resharper_indent_nested_foreach_stmt = true
+resharper_indent_nested_for_stmt = true
+resharper_indent_nested_lock_stmt = true
+resharper_indent_nested_usings_stmt = true
+resharper_indent_nested_while_stmt = true
+resharper_indent_preprocessor_if = outdent
+resharper_keep_existing_declaration_block_arrangement = false
+resharper_keep_existing_embedded_block_arrangement = false
+resharper_keep_existing_enum_arrangement = false
+resharper_place_accessorholder_attribute_on_same_line = false
+resharper_show_autodetect_configure_formatting_tip = false
+resharper_space_within_single_line_array_initializer_braces = false
+resharper_use_heuristics_for_body_style = true
+
+# ReSharper inspection severities
+resharper_arrange_constructor_or_destructor_body_highlighting = none
+resharper_arrange_method_or_operator_body_highlighting = none
+resharper_arrange_redundant_parentheses_highlighting = hint
+resharper_arrange_this_qualifier_highlighting = hint
+resharper_arrange_type_member_modifiers_highlighting = hint
+resharper_arrange_type_modifiers_highlighting = hint
+resharper_built_in_type_reference_style_for_member_access_highlighting = hint
+resharper_built_in_type_reference_style_highlighting = hint
+resharper_redundant_base_qualifier_highlighting = warning
+resharper_suggest_var_or_type_built_in_types_highlighting = hint
+resharper_suggest_var_or_type_elsewhere_highlighting = hint
+resharper_suggest_var_or_type_simple_types_highlighting = hint
+resharper_use_object_or_collection_initializer_highlighting = hint
+
+
#### Analyzers Rules ####
## Microsoft.CodeAnalysis.CSharp.CodeStyle
@@ -295,7 +386,7 @@ dotnet_diagnostic.IDE0005.severity = none
dotnet_diagnostic.IDE0007.severity = warning
# IDE0008: Use explicit type instead of 'var'
-dotnet_diagnostic.IDE0008.severity = warning
+dotnet_diagnostic.IDE0008.severity = none
# IDE0009: Add this or Me qualification
dotnet_diagnostic.IDE0009.severity = warning
diff --git a/src/LogExpert.Core/Classes/Persister/JsonColumnizerPropertyAttribute.cs b/src/LogExpert.Core/Classes/Attributes/JsonColumnizerPropertyAttribute.cs
similarity index 57%
rename from src/LogExpert.Core/Classes/Persister/JsonColumnizerPropertyAttribute.cs
rename to src/LogExpert.Core/Classes/Attributes/JsonColumnizerPropertyAttribute.cs
index af1219fc..46f5267e 100644
--- a/src/LogExpert.Core/Classes/Persister/JsonColumnizerPropertyAttribute.cs
+++ b/src/LogExpert.Core/Classes/Attributes/JsonColumnizerPropertyAttribute.cs
@@ -1,10 +1,10 @@
-namespace LogExpert.Core.Classes.Persister;
+namespace LogExpert.Core.Classes.Attributes;
///
/// Marks a property for inclusion in columnizer JSON serialization.
///
[AttributeUsage(AttributeTargets.Property)]
-public class JsonColumnizerPropertyAttribute : Attribute
+public sealed class JsonColumnizerPropertyAttribute : Attribute
{
}
diff --git a/src/LogExpert.Core/Classes/Filter/FilterParams.cs b/src/LogExpert.Core/Classes/Filter/FilterParams.cs
index dde05dfe..68376518 100644
--- a/src/LogExpert.Core/Classes/Filter/FilterParams.cs
+++ b/src/LogExpert.Core/Classes/Filter/FilterParams.cs
@@ -3,7 +3,7 @@
using System.Drawing;
using System.Text.RegularExpressions;
-using LogExpert.Core.Classes.Persister;
+using LogExpert.Core.Classes.JsonConverters;
using Newtonsoft.Json;
diff --git a/src/LogExpert.Core/Classes/Persister/ColumnizerJsonConverter.cs b/src/LogExpert.Core/Classes/JsonConverters/ColumnizerJsonConverter.cs
similarity index 97%
rename from src/LogExpert.Core/Classes/Persister/ColumnizerJsonConverter.cs
rename to src/LogExpert.Core/Classes/JsonConverters/ColumnizerJsonConverter.cs
index 7f9927c4..344fe37d 100644
--- a/src/LogExpert.Core/Classes/Persister/ColumnizerJsonConverter.cs
+++ b/src/LogExpert.Core/Classes/JsonConverters/ColumnizerJsonConverter.cs
@@ -1,9 +1,11 @@
using System.Reflection;
+using LogExpert.Core.Classes.Attributes;
+
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
-namespace LogExpert.Core.Classes.Persister;
+namespace LogExpert.Core.Classes.JsonConverters;
///
/// Custom JsonConverter for ILogLineColumnizer implementations.
diff --git a/src/LogExpert.Core/Classes/Persister/EncodingJsonConverter.cs b/src/LogExpert.Core/Classes/JsonConverters/EncodingJsonConverter.cs
similarity index 54%
rename from src/LogExpert.Core/Classes/Persister/EncodingJsonConverter.cs
rename to src/LogExpert.Core/Classes/JsonConverters/EncodingJsonConverter.cs
index 95dcc29d..764785b5 100644
--- a/src/LogExpert.Core/Classes/Persister/EncodingJsonConverter.cs
+++ b/src/LogExpert.Core/Classes/JsonConverters/EncodingJsonConverter.cs
@@ -2,7 +2,7 @@
using Newtonsoft.Json;
-namespace LogExpert.Core.Classes.Persister;
+namespace LogExpert.Core.Classes.JsonConverters;
///
/// Custom JsonConverter for Encoding objects.
@@ -15,9 +15,16 @@ public override bool CanConvert (Type objectType)
return typeof(Encoding).IsAssignableFrom(objectType);
}
+ ///
+ /// Serializes the Encoding object to its name.
+ ///
+ ///
+ ///
+ ///
public override void WriteJson (JsonWriter writer, object? value, JsonSerializer serializer)
{
ArgumentNullException.ThrowIfNull(writer);
+
if (value is not Encoding encoding)
{
writer.WriteNull();
@@ -27,9 +34,19 @@ public override void WriteJson (JsonWriter writer, object? value, JsonSerializer
writer.WriteValue(encoding.WebName);
}
+ ///
+ /// Reads a JSON value and converts it to an object.
+ ///
+ /// The to read from. Cannot be .
+ /// The type of the object to deserialize. This parameter is not used in this method.
+ /// The existing value of the object being deserialized. This parameter is not used in this method.
+ /// The calling serializer. This parameter is not used in this method.
+ /// An object corresponding to the JSON value. Returns if the
+ /// JSON value is , empty, or an invalid encoding name.
public override object? ReadJson (JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
ArgumentNullException.ThrowIfNull(reader);
+
if (reader.TokenType == JsonToken.Null)
{
return null;
diff --git a/src/LogExpert.Core/Classes/Persister/PersistenceData.cs b/src/LogExpert.Core/Classes/Persister/PersistenceData.cs
index ca061d31..b771423a 100644
--- a/src/LogExpert.Core/Classes/Persister/PersistenceData.cs
+++ b/src/LogExpert.Core/Classes/Persister/PersistenceData.cs
@@ -1,6 +1,5 @@
-using System.Text;
-
using LogExpert.Core.Classes.Filter;
+using LogExpert.Core.Classes.JsonConverters;
using LogExpert.Core.Entities;
using Newtonsoft.Json;
@@ -21,7 +20,7 @@ public class PersistenceData
public int CurrentLine { get; set; } = -1;
[JsonConverter(typeof(EncodingJsonConverter))]
- public Encoding Encoding { get; set; }
+ public System.Text.Encoding Encoding { get; set; }
public string FileName { get; set; }
diff --git a/src/LogExpert.Core/Classes/Persister/Persister.cs b/src/LogExpert.Core/Classes/Persister/Persister.cs
index 7d202d35..25011252 100644
--- a/src/LogExpert.Core/Classes/Persister/Persister.cs
+++ b/src/LogExpert.Core/Classes/Persister/Persister.cs
@@ -1,5 +1,6 @@
using System.Text;
+using LogExpert.Core.Classes.JsonConverters;
using LogExpert.Core.Config;
using Newtonsoft.Json;
@@ -285,8 +286,16 @@ private static PersistenceData LoadInternal (string fileName)
}
catch (Exception ex) when (ex is JsonSerializationException or
UnauthorizedAccessException or
- IOException)
+ IOException or
+ JsonReaderException)
{
+ //Backup try to load xml instead of json
+ var xmlData = PersisterXML.Load(fileName);
+ if (xmlData != null)
+ {
+ return xmlData;
+ }
+
_logger.Error(ex, $"Error loading persistence data from {fileName}");
return null;
}
diff --git a/src/LogExpert.Core/Classes/Persister/PersisterXML.cs b/src/LogExpert.Core/Classes/Persister/PersisterXML.cs
new file mode 100644
index 00000000..832a8d6a
--- /dev/null
+++ b/src/LogExpert.Core/Classes/Persister/PersisterXML.cs
@@ -0,0 +1,610 @@
+using System.Drawing;
+using System.Text;
+using System.Text.Json;
+using System.Xml;
+
+using LogExpert.Core.Classes.Filter;
+using LogExpert.Core.Entities;
+
+using NLog;
+
+namespace LogExpert.Core.Classes.Persister;
+
+///
+/// Persister for XML format persistence data.
+///
+public static class PersisterXML
+{
+ #region Fields
+
+ private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
+
+ ///
+ /// Reads all filter tab definitions from the given .
+ ///
+ ///
+ /// The root file XmlElement which may contain a direct child element named filterTabs.
+ /// Must not be null.
+ ///
+ ///
+ /// A list of instances. Returns an empty list if no filterTabs element exists.
+ ///
+ ///
+ /// Expected XML structure:
+ ///
+ ///
+ /// ... (persistence related child nodes)
+ ///
+ ///
+ ///
+ /// BASE64(JSON FilterParams)
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// ]]>
+ /// Processing steps:
+ /// - Locates the filterTabs node under .
+ /// - Iterates each child node (expected: filterTab).
+ /// - For each node:
+ /// - Calls to hydrate a nested .
+ /// - Locates tabFilter and deserializes its first (and historically only) entry.
+ /// - Wraps both into a and adds it to the result list.
+ /// - If JSON deserialization of filter parameters fails, the error is logged and the specific tab is skipped.
+ /// Notes:
+ /// - Only the first entry of the deserialized filter list is used because the persisted format supports
+ /// exactly one filter per tab.
+ /// - Returns an empty list if the filterTabs node is absent.
+ ///
+ private static List ReadFilterTabs (XmlElement startNode)
+ {
+ List dataList = [];
+ XmlNode filterTabsNode = startNode.SelectSingleNode("filterTabs");
+ if (filterTabsNode != null)
+ {
+ XmlNodeList filterTabNodeList = filterTabsNode.ChildNodes;
+
+ foreach (XmlNode node in filterTabNodeList)
+ {
+ PersistenceData persistenceData = ReadPersistenceDataFromNode(node);
+ XmlNode filterNode = node.SelectSingleNode("tabFilter");
+
+ if (filterNode != null)
+ {
+ List filterList = ReadFilter(filterNode as XmlElement);
+ FilterTabData data = new()
+ {
+ PersistenceData = persistenceData,
+ FilterParams = filterList[0]
+ };
+
+ dataList.Add(data);
+ }
+ }
+ }
+
+ return dataList;
+ }
+
+ ///
+ /// Reads and deserializes all entries from a given XML element which contains
+ /// a child element named filters.
+ ///
+ ///
+ /// The XML element expected to have a child element filters. This method is used both for
+ /// global filter lists (root file element) and per-tab filters (tabFilter element).
+ /// Structure example:
+ ///
+ ///
+ ///
+ /// BASE64(JSON FilterParams)
+ ///
+ ///
+ /// BASE64(JSON FilterParams)
+ ///
+ ///
+ ///
+ /// ]]>
+ ///
+ ///
+ /// A list of deserialized instances. Returns an empty list if the
+ /// filters element is missing or no valid filter entries are found.
+ ///
+ ///
+ /// Processing steps:
+ /// 1. Locates the filters child node.
+ /// 2. Iterates each filter node.
+ /// 3. For each params child:
+ /// - Decodes its Base64 inner text to bytes.
+ /// - Deserializes JSON to .
+ /// - Calls to finalize state.
+ /// 4. Adds successfully deserialized instances to the result list.
+ /// Errors:
+ /// - during deserialization is logged (entry skipped).
+ /// - Possible from invalid Base64 is not caught here.
+ ///
+ private static List ReadFilter (XmlElement startNode)
+ {
+ List filterList = [];
+
+ if (startNode == null)
+ {
+ return filterList;
+ }
+
+ XmlNode filtersNode = startNode.SelectSingleNode("filters");
+ if (filtersNode != null)
+ {
+ XmlNodeList filterNodeList = filtersNode.ChildNodes;
+ foreach (XmlNode node in filterNodeList)
+ {
+ foreach (XmlNode subNode in node.ChildNodes)
+ {
+ if (subNode.Name.Equals("params", StringComparison.OrdinalIgnoreCase))
+ {
+ var base64Text = subNode.InnerText;
+ var data = Convert.FromBase64String(base64Text);
+ using MemoryStream stream = new(data);
+ try
+ {
+ FilterParams filterParams = JsonSerializer.Deserialize(stream);
+ filterParams.Init();
+ filterList.Add(filterParams);
+ }
+ catch (JsonException ex)
+ {
+ _logger.Error($"Error while deserializing filter params. Exception Message: {ex.Message}");
+ }
+ }
+ }
+ }
+ }
+
+ return filterList;
+ }
+
+ ///
+ /// Loads persistence data from an XML file (internal implementation without exception filtering).
+ ///
+ /// Full path to the XML persistence file to read.
+ ///
+ /// A populated instance. If the expected root node
+ /// (logexpert/file) is missing an empty instance with default values is returned.
+ ///
+ ///
+ /// This method:
+ /// 1. Loads the XML document.
+ /// 2. Selects the node logexpert/file.
+ /// 3. Delegates hydration to .
+ ///
+ private static PersistenceData LoadInternal (string fileName)
+ {
+ XmlDocument xmlDoc = new();
+ xmlDoc.Load(fileName);
+ XmlNode fileNode = xmlDoc.SelectSingleNode("logexpert/file");
+ PersistenceData persistenceData = new();
+ if (fileNode != null)
+ {
+ persistenceData = ReadPersistenceDataFromNode(fileNode);
+ }
+
+ return persistenceData;
+ }
+
+ ///
+ /// Reads persistence-related information (bookmarks, row heights, filters, encoding, options, etc.)
+ /// from a given assumed to represent a file element.
+ ///
+ ///
+ /// The XML node (ideally an ) containing child elements for bookmarks,
+ /// options, filters, filter tabs, and encoding. Must not be null; if the cast to
+ /// fails an empty with default values is returned.
+ ///
+ ///
+ /// A fully populated instance. Collections are initialized to empty lists
+ /// when corresponding XML sections are absent.
+ ///
+ ///
+ /// Processing order:
+ /// 1. Cast node to .
+ /// 2. Bookmarks via .
+ /// 3. Row heights via .
+ /// 4. Options via .
+ /// 5. File attributes: fileName, lineCount.
+ /// 6. Filters via .
+ /// 7. Filter tabs via .
+ /// 8. Encoding via .
+ /// Invalid integers for lineCount will throw .
+ ///
+ private static PersistenceData ReadPersistenceDataFromNode (XmlNode node)
+ {
+ PersistenceData persistenceData = new();
+ var fileElement = node as XmlElement;
+ persistenceData.BookmarkList = ReadBookmarks(fileElement);
+ persistenceData.RowHeightList = ReadRowHeightList(fileElement);
+ ReadOptions(fileElement, persistenceData);
+ persistenceData.FileName = fileElement.GetAttribute("fileName");
+ var sLineCount = fileElement.GetAttribute("lineCount");
+ if (sLineCount != null && sLineCount.Length > 0)
+ {
+ persistenceData.LineCount = int.Parse(sLineCount);
+ }
+
+ persistenceData.FilterParamsList = ReadFilter(fileElement);
+ persistenceData.FilterTabDataList = ReadFilterTabs(fileElement);
+ persistenceData.Encoding = ReadEncoding(fileElement);
+ return persistenceData;
+ }
+
+ ///
+ /// Attempts to resolve the file text from the given XML element.
+ ///
+ ///
+ /// The root file element which may contain an encoding child element:
+ ///
+ /// ]]>
+ /// The element must not be null (no internal null check performed).
+ ///
+ ///
+ /// The resolved when the encoding element exists and its name attribute
+ /// maps to a supported encoding; null if the encoding element is absent or the attribute is missing.
+ /// If the specified name is invalid or not supported an error is logged and is returned.
+ ///
+ ///
+ /// Processing rules:
+ /// - Looks for a direct child element named encoding.
+ /// - Reads its name attribute and calls .
+ /// - Catches and ; logs and falls back to .
+ /// - Does not throw for missing node/attribute; returns null in that case.
+ ///
+ private static Encoding ReadEncoding (XmlElement fileElement)
+ {
+ XmlNode encodingNode = fileElement.SelectSingleNode("encoding");
+ if (encodingNode != null)
+ {
+ XmlAttribute encAttr = encodingNode.Attributes["name"];
+ try
+ {
+ return encAttr == null ? null : Encoding.GetEncoding(encAttr.Value);
+ }
+ catch (ArgumentException e)
+ {
+ _logger.Error(e);
+ return Encoding.Default;
+ }
+ catch (NotSupportedException e)
+ {
+ _logger.Error(e);
+ return Encoding.Default;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// Reads bookmark entries from the given XML element and returns them as a sorted list keyed by line number.
+ ///
+ ///
+ /// Expected XML structure:
+ ///
+ ///
+ ///
+ /// User set bookmark
+ /// 10
+ /// 25
+ ///
+ ///
+ /// 4
+ /// 12
+ ///
+ ///
+ ///
+ /// Processing details:
+ /// - Each bookmark element must have a line attribute that parses to an integer.
+ /// - Optional child element text provides the bookmark text/comment.
+ /// - Required child elements posX and posY define the overlay offset (parsed as integers).
+ /// - Invalid bookmark nodes (missing required data) are skipped and an error is logged.
+ /// - Bookmarks are stored in a keyed by their line number.
+ ///
+ /// The XML element that contains (or has as descendant) the bookmarks element.
+ ///
+ /// A sorted list of instances keyed by line number. Returns an empty list if no
+ /// bookmarks element exists.
+ ///
+ ///
+ /// Thrown if a numeric value (line / posX / posY) cannot be parsed to an integer. This will abort processing of the current bookmark.
+ ///
+ private static SortedList ReadBookmarks (XmlElement startNode)
+ {
+ SortedList bookmarkList = [];
+ XmlNode bookmarksNode = startNode.SelectSingleNode("bookmarks");
+ if (bookmarksNode != null)
+ {
+ XmlNodeList bookmarkNodeList = bookmarksNode.ChildNodes;
+ foreach (XmlNode node in bookmarkNodeList)
+ {
+ string text = null;
+ string posX = null;
+ string posY = null;
+ string line = null;
+
+ foreach (XmlAttribute attr in node.Attributes)
+ {
+ if (attr.Name.Equals("line", StringComparison.OrdinalIgnoreCase))
+ {
+ line = attr.InnerText;
+ }
+ }
+
+ foreach (XmlNode subNode in node.ChildNodes)
+ {
+ if (subNode.Name.Equals("text", StringComparison.OrdinalIgnoreCase))
+ {
+ text = subNode.InnerText;
+ }
+ else if (subNode.Name.Equals("posX", StringComparison.OrdinalIgnoreCase))
+ {
+ posX = subNode.InnerText;
+ }
+ else if (subNode.Name.Equals("posY", StringComparison.OrdinalIgnoreCase))
+ {
+ posY = subNode.InnerText;
+ }
+ }
+
+ if (line == null || posX == null || posY == null)
+ {
+ _logger.Error($"Invalid XML format for bookmark: {node.InnerText}");
+ continue;
+ }
+
+ var lineNum = int.Parse(line);
+
+ Entities.Bookmark bookmark = new(lineNum)
+ {
+ OverlayOffset = new Size(int.Parse(posX), int.Parse(posY))
+ };
+
+ if (text != null)
+ {
+ bookmark.Text = text;
+ }
+
+ bookmarkList.Add(lineNum, bookmark);
+ }
+ }
+
+ return bookmarkList;
+ }
+
+ ///
+ /// Reads row height entries from the given and returns them
+ /// as a sorted list keyed by line number.
+ ///
+ ///
+ /// Expected XML structure:
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// Each rowheight element must contain a line attribute (the line number)
+ /// and a height attribute (the row height value).
+ /// Missing or invalid attributes will throw a during parsing.
+ ///
+ /// The XML element to search within (usually the file element).
+ ///
+ /// A mapping line numbers to instances.
+ /// Returns an empty list if no rowheights node is present.
+ ///
+ private static SortedList ReadRowHeightList (XmlElement startNode)
+ {
+ SortedList rowHeightList = [];
+ XmlNode rowHeightsNode = startNode.SelectSingleNode("rowheights");
+ if (rowHeightsNode != null)
+ {
+ XmlNodeList rowHeightNodeList = rowHeightsNode.ChildNodes;
+ foreach (XmlNode node in rowHeightNodeList)
+ {
+ string height = null;
+ string line = null;
+ foreach (XmlAttribute attr in node.Attributes)
+ {
+ if (attr.Name.Equals("line", StringComparison.OrdinalIgnoreCase))
+ {
+ line = attr.InnerText;
+ }
+ else if (attr.Name.Equals("height", StringComparison.OrdinalIgnoreCase))
+ {
+ height = attr.InnerText;
+ }
+ }
+
+ var lineNum = int.Parse(line);
+ var heightValue = int.Parse(height);
+ rowHeightList.Add(lineNum, new RowHeightEntry(lineNum, heightValue));
+ }
+ }
+
+ return rowHeightList;
+ }
+
+ ///
+ /// Reads configuration options from the specified XML element and populates the provided object with the extracted settings.
+ ///
+ /// This method processes various configuration options such as multi-file settings, current line
+ /// information, filter visibility, and more. It expects the XML structure to contain specific nodes and attributes
+ /// that define these settings. If certain attributes are missing or invalid, default values are applied.
+ /// The XML element containing the configuration options to be read.
+ /// The object to populate with the settings extracted from the XML element.
+ private static void ReadOptions (XmlElement startNode, PersistenceData persistenceData)
+ {
+ XmlNode optionsNode = startNode.SelectSingleNode("options");
+ var value = GetOptionsAttribute(optionsNode, "multifile", "enabled");
+ persistenceData.MultiFile = value != null && value.Equals("1", StringComparison.OrdinalIgnoreCase);
+ persistenceData.MultiFilePattern = GetOptionsAttribute(optionsNode, "multifile", "pattern");
+ value = GetOptionsAttribute(optionsNode, "multifile", "maxDays");
+ try
+ {
+ persistenceData.MultiFileMaxDays = value != null ? short.Parse(value) : 0;
+ }
+ catch (Exception ex) when (ex is ArgumentNullException or
+ FormatException or
+ OverflowException)
+ {
+ persistenceData.MultiFileMaxDays = 0;
+ }
+
+ XmlNode multiFileNode = optionsNode.SelectSingleNode("multifile");
+ if (multiFileNode != null)
+ {
+ XmlNodeList multiFileNodeList = multiFileNode.ChildNodes;
+ foreach (XmlNode node in multiFileNodeList)
+ {
+ string fileName = null;
+ foreach (XmlAttribute attr in node.Attributes)
+ {
+ if (attr.Name.Equals("fileName", StringComparison.OrdinalIgnoreCase))
+ {
+ fileName = attr.InnerText;
+ }
+ }
+
+ persistenceData.MultiFileNames.Add(fileName);
+ }
+ }
+
+ value = GetOptionsAttribute(optionsNode, "currentline", "line");
+ if (value != null)
+ {
+ persistenceData.CurrentLine = int.Parse(value);
+ }
+
+ value = GetOptionsAttribute(optionsNode, "firstDisplayedLine", "line");
+ if (value != null)
+ {
+ persistenceData.FirstDisplayedLine = int.Parse(value);
+ }
+
+ value = GetOptionsAttribute(optionsNode, "filter", "visible");
+ persistenceData.FilterVisible = value != null && value.Equals("1", StringComparison.OrdinalIgnoreCase);
+ value = GetOptionsAttribute(optionsNode, "filter", "advanced");
+ persistenceData.FilterAdvanced = value != null && value.Equals("1", StringComparison.OrdinalIgnoreCase);
+ value = GetOptionsAttribute(optionsNode, "filter", "position");
+ if (value != null)
+ {
+ persistenceData.FilterPosition = int.Parse(value);
+ }
+
+ value = GetOptionsAttribute(optionsNode, "bookmarklist", "visible");
+ persistenceData.BookmarkListVisible = value != null && value.Equals("1", StringComparison.OrdinalIgnoreCase);
+ value = GetOptionsAttribute(optionsNode, "bookmarklist", "position");
+ if (value != null)
+ {
+ persistenceData.BookmarkListPosition = int.Parse(value);
+ }
+
+ value = GetOptionsAttribute(optionsNode, "followTail", "enabled");
+ persistenceData.FollowTail = value != null && value.Equals("1", StringComparison.OrdinalIgnoreCase);
+
+ value = GetOptionsAttribute(optionsNode, "bookmarkCommentColumn", "visible");
+ persistenceData.ShowBookmarkCommentColumn = value != null && value.Equals("1", StringComparison.OrdinalIgnoreCase);
+
+ value = GetOptionsAttribute(optionsNode, "filterSaveList", "visible");
+ persistenceData.FilterSaveListVisible = value != null && value.Equals("1", StringComparison.OrdinalIgnoreCase);
+
+ XmlNode tabNode = startNode.SelectSingleNode("tab");
+ if (tabNode != null)
+ {
+ persistenceData.TabName = (tabNode as XmlElement).GetAttribute("name");
+ }
+
+ XmlNode columnizerNode = startNode.SelectSingleNode("columnizer");
+ if (columnizerNode != null)
+ {
+ persistenceData.ColumnizerName = (columnizerNode as XmlElement).GetAttribute("name");
+ }
+
+ XmlNode highlightGroupNode = startNode.SelectSingleNode("highlightGroup");
+ if (highlightGroupNode != null)
+ {
+ persistenceData.HighlightGroupName = (highlightGroupNode as XmlElement).GetAttribute("name");
+ }
+ }
+
+ ///
+ /// Retrieves the value of a specified attribute from a child element within the given .
+ ///
+ ///
+ /// The parent XML node expected to contain the child element identified by .
+ /// Must not be null; otherwise a will occur before this method is called.
+ ///
+ ///
+ /// The name of the child element to search for (e.g. "multifile", "filter", "bookmarklist").
+ ///
+ ///
+ /// The name of the attribute whose value should be returned (e.g. "enabled", "pattern", "visible").
+ ///
+ ///
+ /// The attribute value as a string if the child element exists and is an and the attribute is present;
+ /// otherwise null.
+ ///
+ ///
+ /// This method performs a direct XPath child lookup using .
+ /// It does not perform any conversion of the returned value. Callers are responsible for parsing or validating the result.
+ ///
+ private static string GetOptionsAttribute (XmlNode optionsNode, string elementName, string attrName)
+ {
+ XmlNode node = optionsNode.SelectSingleNode(elementName);
+ if (node == null)
+ {
+ return null;
+ }
+
+ if (node is XmlElement element)
+ {
+ var valueAttr = element.GetAttribute(attrName);
+ return valueAttr;
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// Loads persistence data from the specified XML file.
+ ///
+ /// Full path to the persistence XML file.
+ ///
+ /// A populated instance if loading succeeds; otherwise null
+ /// when the file cannot be read or parsed (XML/IO/security related issues are logged).
+ ///
+ ///
+ /// Only XML format is attempted. Any , ,
+ /// or is caught and logged; in these cases null is returned.
+ ///
+ public static PersistenceData Load (string fileName)
+ {
+ try
+ {
+ return LoadInternal(fileName);
+ }
+ catch (Exception xmlParsingException) when (xmlParsingException is XmlException or
+ UnauthorizedAccessException or
+ IOException)
+ {
+ _logger.Error(xmlParsingException, $"Error loading persistence data from {fileName}, unknown format, parsing xml or json was not possible");
+ return null;
+ }
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/LogExpert.Core/Classes/Persister/ProjectPersister.cs b/src/LogExpert.Core/Classes/Persister/ProjectPersister.cs
index 726f1566..b2ae8f17 100644
--- a/src/LogExpert.Core/Classes/Persister/ProjectPersister.cs
+++ b/src/LogExpert.Core/Classes/Persister/ProjectPersister.cs
@@ -12,6 +12,11 @@ public static class ProjectPersister
#region Public methods
+ ///
+ /// Loads the project session data from a specified file.
+ ///
+ ///
+ ///
public static ProjectData LoadProjectData (string projectFileName)
{
try
@@ -25,13 +30,22 @@ public static ProjectData LoadProjectData (string projectFileName)
return JsonConvert.DeserializeObject(json, settings);
}
catch (Exception ex) when (ex is UnauthorizedAccessException or
- IOException)
+ IOException or
+ JsonSerializationException)
{
- _logger.Error(ex, $"Error loading persistence data from {projectFileName}");
- return new ProjectData();
+
+ _logger.Warn($"Error loading persistence data from {projectFileName}, trying old xml version");
+ return ProjectPersisterXML.LoadProjectData(projectFileName);
}
}
+ ///
+ /// Saves the specified project data to a file in JSON format.
+ ///
+ /// The method serializes the into a JSON string with indented
+ /// formatting and writes it to the specified using UTF-8 encoding.
+ /// The path to the file where the project data will be saved. Cannot be null or empty.
+ /// The project data to be serialized and saved. Cannot be null.
public static void SaveProjectData (string projectFileName, ProjectData projectData)
{
var settings = new JsonSerializerSettings
diff --git a/src/LogExpert.Core/Classes/Persister/ProjectPersisterXML.cs b/src/LogExpert.Core/Classes/Persister/ProjectPersisterXML.cs
new file mode 100644
index 00000000..618b925e
--- /dev/null
+++ b/src/LogExpert.Core/Classes/Persister/ProjectPersisterXML.cs
@@ -0,0 +1,53 @@
+using System.Xml;
+
+using NLog;
+
+namespace LogExpert.Core.Classes.Persister;
+
+public static class ProjectPersisterXML
+{
+ private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
+
+ ///
+ /// Loads project data from the specified XML file.
+ ///
+ /// The method reads the XML file to extract file names and layout information. If the XML file
+ /// contains a layout element, its inner XML is stored in the TabLayoutXml property of the returned
+ /// object. If any exception occurs during the loading process, an error is logged, and an
+ /// empty object is returned.
+ /// The path to the XML file containing the project data.
+ /// A object populated with file names and layout information from the XML file. If an
+ /// error occurs during loading, an empty object is returned.
+ public static ProjectData LoadProjectData (string projectFileName)
+ {
+ var projectData = new ProjectData();
+ var xmlDoc = new XmlDocument();
+ try
+ {
+ xmlDoc.Load(projectFileName);
+ var fileList = xmlDoc.GetElementsByTagName("member");
+
+ foreach (XmlNode fileNode in fileList)
+ {
+ var fileElement = fileNode as XmlElement;
+ var fileName = fileElement.GetAttribute("fileName");
+ projectData.FileNames.Add(fileName);
+ }
+
+ var layoutElements = xmlDoc.GetElementsByTagName("layout");
+ if (layoutElements.Count > 0)
+ {
+ projectData.TabLayoutXml = layoutElements[0].InnerXml;
+ }
+
+ return projectData;
+ }
+ catch (Exception xmlParsingException) when (xmlParsingException is XmlException or
+ UnauthorizedAccessException or
+ IOException)
+ {
+ _logger.Error(xmlParsingException, $"Error loading persistence data from {projectFileName}, unknown format, parsing xml or json was not possible");
+ return new ProjectData();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/LogExpert.Core/Config/ImportResult.cs b/src/LogExpert.Core/Config/ImportResult.cs
new file mode 100644
index 00000000..ae1d56e3
--- /dev/null
+++ b/src/LogExpert.Core/Config/ImportResult.cs
@@ -0,0 +1,59 @@
+namespace LogExpert.Core.Config;
+
+///
+/// Result of a settings import operation
+///
+public class ImportResult
+{
+ ///
+ /// Indicates whether the import operation was successful.
+ ///
+ public bool Success { get; set; }
+
+ ///
+ /// The error message describing why the import failed.
+ /// Populated when is false and is false.
+ ///
+ public string ErrorMessage { get; set; }
+
+ ///
+ /// The title for the error message.
+ /// Populated when is false and is false.
+ ///
+ public string ErrorTitle { get; set; }
+
+ ///
+ /// Indicates whether the import operation requires user confirmation to proceed.
+ /// When true, and are populated.
+ ///
+ public bool RequiresUserConfirmation { get; set; }
+
+ ///
+ /// The message to display when user confirmation is required.
+ /// Populated when is true.
+ ///
+ public string ConfirmationMessage { get; set; }
+
+ ///
+ /// The title for the confirmation message.
+ /// Populated when is true.
+ ///
+ public string ConfirmationTitle { get; set; }
+
+ public static ImportResult Successful () => new() { Success = true };
+
+ public static ImportResult Failed (string title, string message) => new()
+ {
+ Success = false,
+ ErrorTitle = title,
+ ErrorMessage = message
+ };
+
+ public static ImportResult RequiresConfirmation (string title, string message) => new()
+ {
+ Success = false,
+ RequiresUserConfirmation = true,
+ ConfirmationTitle = title,
+ ConfirmationMessage = message
+ };
+}
diff --git a/src/LogExpert.Core/Config/LoadResult.cs b/src/LogExpert.Core/Config/LoadResult.cs
new file mode 100644
index 00000000..f2cf515e
--- /dev/null
+++ b/src/LogExpert.Core/Config/LoadResult.cs
@@ -0,0 +1,89 @@
+namespace LogExpert.Core.Config;
+
+///
+/// Result of a settings load operation
+///
+public class LoadResult
+{
+ ///
+ /// The loaded settings object. Always populated on success or recovery.
+ ///
+
+ public Settings Settings { get; set; }
+
+ ///
+ /// Indicates whether the settings were loaded from a backup file due to a failure loading the primary file.
+ ///
+ public bool LoadedFromBackup { get; set; }
+
+ ///
+ /// A message describing the recovery process. Only meaningful when is true.
+ ///
+ public string RecoveryMessage { get; set; }
+
+ ///
+ /// A title for the recovery message dialog. Only meaningful when is true.
+ ///
+ public string RecoveryTitle { get; set; }
+
+ ///
+ /// Indicates a critical failure occurred during loading. When true, loading could not complete normally.
+ ///
+ public bool CriticalFailure { get; set; }
+
+ ///
+ /// A message describing the critical failure. Only meaningful when is true.
+ ///
+ public string CriticalMessage { get; set; }
+
+ ///
+ /// A title for the critical failure dialog. Only meaningful when is true.
+ ///
+ public string CriticalTitle { get; set; }
+
+ ///
+ /// Indicates whether user input is required to proceed after a critical failure.
+ ///
+ public bool RequiresUserChoice { get; set; }
+
+ ///
+ /// Creates a successful LoadResult.
+ ///
+ ///
+ ///
+ public static LoadResult Success (Settings settings) => new()
+ {
+ Settings = settings
+ };
+
+ ///
+ /// Creates a LoadResult indicating settings were loaded from a backup.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static LoadResult FromBackup (Settings settings, string message, string title) => new()
+ {
+ Settings = settings,
+ LoadedFromBackup = true,
+ RecoveryMessage = message,
+ RecoveryTitle = title
+ };
+
+ ///
+ /// Creates a LoadResult indicating a critical failure occurred.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static LoadResult Critical (Settings settings, string title, string message) => new()
+ {
+ Settings = settings,
+ CriticalFailure = true,
+ CriticalTitle = title,
+ CriticalMessage = message,
+ RequiresUserChoice = true
+ };
+}
\ No newline at end of file
diff --git a/src/LogExpert.Core/Config/Preferences.cs b/src/LogExpert.Core/Config/Preferences.cs
index 779e6fb8..fece418d 100644
--- a/src/LogExpert.Core/Config/Preferences.cs
+++ b/src/LogExpert.Core/Config/Preferences.cs
@@ -26,7 +26,7 @@ public class Preferences
public List ToolEntries { get; set; } = [];
- public DragOrientationsEnum TimestampControlDragOrientation { get; set; } = DragOrientationsEnum.Horizontal;
+ public DragOrientations TimestampControlDragOrientation { get; set; } = DragOrientations.Horizontal;
public bool TimestampControl { get; set; }
diff --git a/src/LogExpert.Core/Enums/DragOrientations.cs b/src/LogExpert.Core/Enums/DragOrientations.cs
new file mode 100644
index 00000000..14a00a37
--- /dev/null
+++ b/src/LogExpert.Core/Enums/DragOrientations.cs
@@ -0,0 +1,8 @@
+namespace LogExpert.Core.Enums;
+
+public enum DragOrientations
+{
+ Horizontal,
+ Vertical,
+ InvertedVertical
+}
diff --git a/src/LogExpert.Core/Enums/DragOrientationsEnum.cs b/src/LogExpert.Core/Enums/DragOrientationsEnum.cs
deleted file mode 100644
index fb6465c4..00000000
--- a/src/LogExpert.Core/Enums/DragOrientationsEnum.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace LogExpert.Core.Enums;
-
-public enum DragOrientationsEnum
-{
- Horizontal,
- Vertical,
- InvertedVertical
-}
diff --git a/src/LogExpert.Core/Interface/IConfigManager.cs b/src/LogExpert.Core/Interface/IConfigManager.cs
index 93a079dd..9abfcdf0 100644
--- a/src/LogExpert.Core/Interface/IConfigManager.cs
+++ b/src/LogExpert.Core/Interface/IConfigManager.cs
@@ -20,7 +20,7 @@ public interface IConfigManager
void Export (FileInfo fileInfo);
- void Import (FileInfo fileInfo, ExportImportFlags importFlags);
+ ImportResult Import (FileInfo fileInfo, ExportImportFlags importFlags);
void ImportHighlightSettings (FileInfo fileInfo, ExportImportFlags importFlags);
diff --git a/src/LogExpert.Core/LogExpert.Core.csproj b/src/LogExpert.Core/LogExpert.Core.csproj
index 094d3861..8b8e1e8f 100644
--- a/src/LogExpert.Core/LogExpert.Core.csproj
+++ b/src/LogExpert.Core/LogExpert.Core.csproj
@@ -3,6 +3,7 @@
net8.0
true
+ LogExpert.Core
diff --git a/src/LogExpert.Tests/ColumnizerJsonConverterTests.cs b/src/LogExpert.Tests/ColumnizerJsonConverterTests.cs
index 0a269035..436b1dd2 100644
--- a/src/LogExpert.Tests/ColumnizerJsonConverterTests.cs
+++ b/src/LogExpert.Tests/ColumnizerJsonConverterTests.cs
@@ -1,4 +1,5 @@
-using LogExpert.Core.Classes.Persister;
+using LogExpert.Core.Classes.Attributes;
+using LogExpert.Core.Classes.JsonConverters;
using Newtonsoft.Json;
diff --git a/src/LogExpert.Tests/ConfigManagerTest.cs b/src/LogExpert.Tests/ConfigManagerTest.cs
new file mode 100644
index 00000000..fe9f6b32
--- /dev/null
+++ b/src/LogExpert.Tests/ConfigManagerTest.cs
@@ -0,0 +1,655 @@
+using System.Reflection;
+
+using LogExpert.Config;
+using LogExpert.Core.Classes.Filter;
+using LogExpert.Core.Config;
+using LogExpert.Core.Entities;
+
+using Newtonsoft.Json;
+
+using NUnit.Framework;
+
+namespace LogExpert.Tests;
+
+///
+/// Unit tests for ConfigManager settings loss prevention fixes.
+/// Tests all 4 Priority 1 implementations: Import Validation, Atomic Write, Deserialization Recovery, Settings Validation.
+///
+[TestFixture]
+public class ConfigManagerTest
+{
+ private string _testDir;
+ private FileInfo _testSettingsFile;
+
+ [SetUp]
+ public void SetUp ()
+ {
+ // Create isolated test directory for each test
+ _testDir = Path.Combine(Path.GetTempPath(), "LogExpert_Test_" + Guid.NewGuid().ToString("N"));
+ _ = Directory.CreateDirectory(_testDir);
+ _testSettingsFile = new FileInfo(Path.Combine(_testDir, "settings.json"));
+ }
+
+ [TearDown]
+ public void TearDown ()
+ {
+ // Cleanup test directory
+ if (Directory.Exists(_testDir))
+ {
+ try
+ {
+ Directory.Delete(_testDir, recursive: true);
+ }
+ catch (IOException)
+ {
+ // Ignore IO errors during cleanup
+ }
+ catch (UnauthorizedAccessException)
+ {
+ // Ignore access errors during cleanup
+ }
+ }
+ }
+
+ #region Helper Methods
+
+ ///
+ /// Invokes a private static method using reflection.
+ ///
+ private T InvokePrivateStaticMethod (string methodName, params object[] parameters)
+ {
+ MethodInfo? method = typeof(ConfigManager).GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static);
+
+ return method == null
+ ? throw new Exception($"Static method {methodName} not found")
+ : (T)method.Invoke(null, parameters);
+ }
+
+ ///
+ /// Invokes a private instance method using reflection.
+ ///
+ private T InvokePrivateInstanceMethod (string methodName, params object[] parameters)
+ {
+ MethodInfo? method = typeof(ConfigManager).GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance);
+
+ return method == null
+ ? throw new Exception($"Instance method {methodName} not found")
+ : (T)method.Invoke(ConfigManager.Instance, parameters);
+ }
+
+ ///
+ /// Invokes a private instance method with no return value using reflection.
+ ///
+ private void InvokePrivateInstanceMethod (string methodName, params object[] parameters)
+ {
+ MethodInfo? method = typeof(ConfigManager).GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance)
+ ?? throw new Exception($"Instance method {methodName} not found");
+
+ method.Invoke(ConfigManager.Instance, parameters);
+ }
+
+ ///
+ /// Creates a basic test Settings object with valid defaults.
+ ///
+ private Settings CreateTestSettings ()
+ {
+ var settings = new Settings
+ {
+ Preferences = new Preferences()
+ };
+
+ return settings;
+ }
+
+ ///
+ /// Creates a populated Settings object with sample data.
+ ///
+ private Settings CreatePopulatedSettings ()
+ {
+ Settings settings = CreateTestSettings();
+ settings.FilterList.Add(new FilterParams { SearchText = "ERROR" });
+ settings.FilterList.Add(new FilterParams { SearchText = "WARNING" });
+ settings.SearchHistoryList.Add("exception");
+ settings.SearchHistoryList.Add("error");
+ settings.Preferences.HighlightGroupList.Add(new HighlightGroup { GroupName = "Errors" });
+ return settings;
+ }
+
+ #endregion
+
+ #region Phase 1: Import Validation Tests
+
+ [Test]
+ [Category("ImportValidation")]
+ [Description("SettingsAreEmptyOrDefault should return true for null settings")]
+ public void SettingsAreEmptyOrDefault_NullSettings_ReturnsTrue ()
+ {
+ // Arrange
+ Settings settings = null;
+
+ // Act
+ bool result = InvokePrivateStaticMethod("SettingsAreEmptyOrDefault", settings);
+
+ // Assert
+ Assert.That(result, Is.True, "Null settings should be detected as empty/default");
+ }
+
+ [Test]
+ [Category("ImportValidation")]
+ [Description("SettingsAreEmptyOrDefault should return true for empty settings")]
+ public void SettingsAreEmptyOrDefault_EmptySettings_ReturnsTrue ()
+ {
+ // Arrange
+ Settings settings = CreateTestSettings();
+
+ // Act
+ bool result = InvokePrivateStaticMethod("SettingsAreEmptyOrDefault", settings);
+
+ // Assert
+ Assert.That(result, Is.True, "Empty settings should be detected as empty/default");
+ }
+
+ [Test]
+ [Category("ImportValidation")]
+ [Description("SettingsAreEmptyOrDefault should return false for settings with filters")]
+ public void SettingsAreEmptyOrDefault_SettingsWithFilters_ReturnsFalse ()
+ {
+ // Arrange
+ Settings settings = CreateTestSettings();
+ settings.FilterList.Add(new FilterParams { SearchText = "TEST_FILTER" });
+
+ // Act
+ bool result = InvokePrivateStaticMethod("SettingsAreEmptyOrDefault", settings);
+
+ // Assert
+ Assert.That(result, Is.False, "Settings with filters should not be empty/default");
+ }
+
+ [Test]
+ [Category("ImportValidation")]
+ [Description("SettingsAreEmptyOrDefault should return false for settings with search history")]
+ public void SettingsAreEmptyOrDefault_SettingsWithHistory_ReturnsFalse ()
+ {
+ // Arrange
+ Settings settings = CreateTestSettings();
+ settings.SearchHistoryList.Add("test search");
+
+ // Act
+ bool result = InvokePrivateStaticMethod("SettingsAreEmptyOrDefault", settings);
+
+ // Assert
+ Assert.That(result, Is.False, "Settings with search history should not be empty/default");
+ }
+
+ [Test]
+ [Category("ImportValidation")]
+ [Description("SettingsAreEmptyOrDefault should return false for settings with highlights")]
+ public void SettingsAreEmptyOrDefault_SettingsWithHighlights_ReturnsFalse ()
+ {
+ // Arrange
+ Settings settings = CreateTestSettings();
+ settings.Preferences.HighlightGroupList.Add(new HighlightGroup { GroupName = "Test" });
+
+ // Act
+ bool result = InvokePrivateStaticMethod("SettingsAreEmptyOrDefault", settings);
+
+ // Assert
+ Assert.That(result, Is.False, "Settings with highlights should not be empty/default");
+ }
+
+ [Test]
+ [Category("ImportValidation")]
+ [Description("ValidateSettings should handle null settings gracefully")]
+ public void ValidateSettings_NullSettings_ReturnsFalse ()
+ {
+ // Arrange
+ Settings settings = null;
+
+ // Act
+ bool result = InvokePrivateInstanceMethod("ValidateSettings", settings);
+
+ // Assert
+ Assert.That(result, Is.False, "Null settings should fail validation");
+ }
+
+ [Test]
+ [Category("ImportValidation")]
+ [Description("ValidateSettings should return true for valid populated settings")]
+ public void ValidateSettings_ValidPopulatedSettings_ReturnsTrue ()
+ {
+ // Arrange
+ Settings settings = CreatePopulatedSettings();
+
+ // Act
+ bool result = InvokePrivateInstanceMethod("ValidateSettings", settings);
+
+ // Assert
+ Assert.That(result, Is.True, "Valid populated settings should pass validation");
+ }
+
+ [Test]
+ [Category("ImportValidation")]
+ [Description("ValidateSettings should return true for valid empty settings")]
+ public void ValidateSettings_ValidEmptySettings_ReturnsTrue ()
+ {
+ // Arrange
+ Settings settings = CreateTestSettings();
+
+ // Act
+ bool result = InvokePrivateInstanceMethod("ValidateSettings", settings);
+
+ // Assert
+ Assert.That(result, Is.True, "Valid empty settings should pass validation (may log warning)");
+ }
+
+ #endregion
+
+ #region Phase 2: Atomic Write Tests
+
+ [Test]
+ [Category("AtomicWrite")]
+ [Description("SaveAsJSON should create main file and cleanup temp file")]
+ public void SaveAsJSON_CreatesMainFileAndCleanupsTempFile ()
+ {
+ // Arrange
+ Settings settings = CreatePopulatedSettings();
+ settings.AlwaysOnTop = true;
+
+ // Act
+ InvokePrivateInstanceMethod("SaveAsJSON", _testSettingsFile, settings);
+
+ // Assert
+ string tempFile = _testSettingsFile.FullName + ".tmp";
+ Assert.That(File.Exists(tempFile), Is.False, "Temp file should be cleaned up");
+ Assert.That(_testSettingsFile.Exists, Is.True, "Main file should exist");
+
+ // Verify content
+ string json = File.ReadAllText(_testSettingsFile.FullName);
+ Assert.That(json, Does.Contain("AlwaysOnTop"));
+ Assert.That(json, Does.Contain("TEST_FILTER").Or.Contain("ERROR"));
+ }
+
+ [Test]
+ [Category("AtomicWrite")]
+ [Description("SaveAsJSON should create backup file on second save")]
+ public void SaveAsJSON_CreatesBackupFile ()
+ {
+ // Arrange
+ Settings settings1 = CreateTestSettings();
+ settings1.AlwaysOnTop = true;
+ InvokePrivateInstanceMethod("SaveAsJSON", _testSettingsFile, settings1);
+
+ Settings settings2 = CreateTestSettings();
+ settings2.AlwaysOnTop = false;
+
+ // Act
+ InvokePrivateInstanceMethod("SaveAsJSON", _testSettingsFile, settings2);
+
+ // Assert
+ string backupFile = _testSettingsFile.FullName + ".bak";
+ Assert.That(File.Exists(backupFile), Is.True, "Backup file should exist");
+
+ string backupContent = File.ReadAllText(backupFile);
+ Settings backupSettings = JsonConvert.DeserializeObject(backupContent);
+ Assert.That(backupSettings.AlwaysOnTop, Is.True, "Backup should contain previous settings");
+
+ string mainContent = File.ReadAllText(_testSettingsFile.FullName);
+ Settings mainSettings = JsonConvert.DeserializeObject(mainContent);
+ Assert.That(mainSettings.AlwaysOnTop, Is.False, "Main file should contain new settings");
+ }
+
+ [Test]
+ [Category("AtomicWrite")]
+ [Description("SaveAsJSON should not backup empty/zero-byte files")]
+ public void SaveAsJSON_DoesNotBackupEmptyFile ()
+ {
+ // Arrange
+ File.WriteAllText(_testSettingsFile.FullName, ""); // Create empty file
+ Settings settings = CreateTestSettings();
+
+ // Act
+ InvokePrivateInstanceMethod("SaveAsJSON", _testSettingsFile, settings);
+
+ // Assert
+ string backupFile = _testSettingsFile.FullName + ".bak";
+ Assert.That(File.Exists(backupFile), Is.False, "Empty file should not be backed up");
+ Assert.That(_testSettingsFile.Exists, Is.True, "Main file should exist");
+ Assert.That(new FileInfo(_testSettingsFile.FullName).Length, Is.GreaterThan(0), "Main file should not be empty");
+ }
+
+ [Test]
+ [Category("AtomicWrite")]
+ [Description("SaveAsJSON should save complete valid JSON that can be deserialized")]
+ public void SaveAsJSON_SavesCompleteValidJSON ()
+ {
+ // Arrange
+ Settings settings = CreateTestSettings();
+ settings.FilterList.Add(new FilterParams { SearchText = "TEST_FILTER_123" });
+ settings.SearchHistoryList.Add("TEST_SEARCH_456");
+ settings.AlwaysOnTop = true;
+
+ // Act
+ InvokePrivateInstanceMethod("SaveAsJSON", _testSettingsFile, settings);
+
+ // Assert
+ Assert.That(_testSettingsFile.Exists, Is.True);
+ string json = File.ReadAllText(_testSettingsFile.FullName);
+
+ // Verify content present
+ Assert.That(json, Does.Contain("TEST_FILTER_123"));
+ Assert.That(json, Does.Contain("TEST_SEARCH_456"));
+
+ // Verify can deserialize
+ Settings loaded = null;
+ Assert.DoesNotThrow(() => loaded = JsonConvert.DeserializeObject(json), "Saved JSON should be valid and deserializable");
+
+ Assert.That(loaded, Is.Not.Null);
+ Assert.That(loaded.FilterList.Count, Is.EqualTo(1));
+ Assert.That(loaded.FilterList[0].SearchText, Is.EqualTo("TEST_FILTER_123"));
+ Assert.That(loaded.SearchHistoryList.Count, Is.EqualTo(1));
+ Assert.That(loaded.AlwaysOnTop, Is.True);
+ }
+
+ [Test]
+ [Category("AtomicWrite")]
+ [Description("SaveAsJSON validation should prevent saving null settings")]
+ public void SaveAsJSON_ValidationFailure_PreventsNullSettingsSave ()
+ {
+ // Arrange
+ Settings settings = null;
+
+ // Act & Assert
+ _ = Assert.Throws(() => InvokePrivateInstanceMethod("SaveAsJSON", _testSettingsFile, settings), "Saving null settings should throw exception");
+
+ Assert.That(_testSettingsFile.Exists, Is.False, "File should not be created if validation fails");
+ }
+
+ [Test]
+ [Category("AtomicWrite")]
+ [Description("SaveAsJSON should maintain file integrity across multiple saves")]
+ public void SaveAsJSON_MultipleSaves_MaintainsIntegrity ()
+ {
+ // Arrange & Act - Multiple saves
+ for (int i = 0; i < 5; i++)
+ {
+ Settings settings = CreateTestSettings();
+ settings.FilterList.Add(new FilterParams { SearchText = $"FILTER_{i}" });
+ InvokePrivateInstanceMethod("SaveAsJSON", _testSettingsFile, settings);
+ }
+
+ // Assert
+ Assert.That(_testSettingsFile.Exists, Is.True);
+ string json = File.ReadAllText(_testSettingsFile.FullName);
+ Settings? loaded = JsonConvert.DeserializeObject(json);
+
+ Assert.That(loaded, Is.Not.Null);
+ Assert.That(loaded.FilterList.Count, Is.EqualTo(1), "Last save should have 1 filter");
+ Assert.That(loaded.FilterList[0].SearchText, Is.EqualTo("FILTER_4"), "Should have last filter");
+
+ // Verify backup has previous version
+ string backupFile = _testSettingsFile.FullName + ".bak";
+ if (File.Exists(backupFile))
+ {
+ string backupJson = File.ReadAllText(backupFile);
+ Settings? backupLoaded = JsonConvert.DeserializeObject(backupJson);
+ Assert.That(backupLoaded.FilterList[0].SearchText, Is.EqualTo("FILTER_3"), "Backup should have previous version");
+ }
+ }
+
+ #endregion
+
+ #region Phase 3: Deserialization Recovery Tests
+
+ [Test]
+ [Category("DeserializationRecovery")]
+ [Description("LoadOrCreateNew should load valid settings file successfully")]
+ public void LoadOrCreateNew_ValidFile_LoadsSuccessfully ()
+ {
+ // Arrange
+ Settings settings = CreatePopulatedSettings();
+ settings.FilterList.Clear();
+ settings.FilterList.Add(new FilterParams { SearchText = "VALID_FILTER_TEST" });
+ string json = JsonConvert.SerializeObject(settings);
+ File.WriteAllText(_testSettingsFile.FullName, json);
+
+ // Act
+ LoadResult loadResult = InvokePrivateInstanceMethod("LoadOrCreateNew", _testSettingsFile);
+
+ // Assert
+ Assert.That(loadResult, Is.Not.Null);
+ Assert.That(loadResult.Settings, Is.Not.Null);
+ Assert.That(loadResult.LoadedFromBackup, Is.False, "Should not load from backup for valid file");
+ Assert.That(loadResult.CriticalFailure, Is.False, "Should not have critical failure");
+ Assert.That(loadResult.Settings.FilterList.Count, Is.EqualTo(1));
+ Assert.That(loadResult.Settings.FilterList[0].SearchText, Is.EqualTo("VALID_FILTER_TEST"));
+ }
+
+ [Test]
+ [Category("DeserializationRecovery")]
+ [Description("LoadOrCreateNew should handle missing file by creating new settings")]
+ public void LoadOrCreateNew_MissingFile_CreatesNewSettings ()
+ {
+ // Arrange - file doesn't exist
+
+ // Act
+ LoadResult loadResult = InvokePrivateInstanceMethod("LoadOrCreateNew", (FileInfo)null);
+
+ // Assert
+ Assert.That(loadResult, Is.Not.Null);
+ Assert.That(loadResult.Settings, Is.Not.Null, "Should create new settings when file is null");
+ Assert.That(loadResult.Settings.Preferences, Is.Not.Null, "Settings should have preferences initialized");
+ Assert.That(loadResult.LoadedFromBackup, Is.False);
+ Assert.That(loadResult.CriticalFailure, Is.False);
+ }
+
+ [Test]
+ [Category("DeserializationRecovery")]
+ [Description("LoadOrCreateNew should handle empty file gracefully")]
+ public void LoadOrCreateNew_EmptyFile_HandlesGracefully ()
+ {
+ // Arrange
+ File.WriteAllText(_testSettingsFile.FullName, "");
+
+ // Act
+ LoadResult loadResult = InvokePrivateInstanceMethod("LoadOrCreateNew", _testSettingsFile);
+
+ // Assert
+ Assert.That(loadResult, Is.Not.Null);
+ Assert.That(loadResult.Settings, Is.Not.Null, "Should return valid settings object, not null");
+ Assert.That(loadResult.Settings.Preferences, Is.Not.Null, "Settings should have preferences");
+ // Empty file triggers recovery, may create new settings
+ }
+
+ [Test]
+ [Category("DeserializationRecovery")]
+ [Description("LoadOrCreateNew should handle null JSON deserialization result")]
+ public void LoadOrCreateNew_NullDeserializationResult_HandlesGracefully ()
+ {
+ // Arrange
+ File.WriteAllText(_testSettingsFile.FullName, "null");
+
+ // Act
+ LoadResult loadResult = InvokePrivateInstanceMethod("LoadOrCreateNew", _testSettingsFile);
+
+ // Assert
+ Assert.That(loadResult, Is.Not.Null);
+ Assert.That(loadResult.Settings, Is.Not.Null, "Should not return null settings");
+ Assert.That(loadResult.Settings.Preferences, Is.Not.Null);
+ }
+
+ [Test]
+ [Category("DeserializationRecovery")]
+ [Description("LoadOrCreateNew should recover from backup when main file is corrupt")]
+ public void LoadOrCreateNew_CorruptFileWithBackup_RecoversFromBackup ()
+ {
+ // Arrange - Create good backup
+ Settings goodSettings = CreateTestSettings();
+ goodSettings.FilterList.Add(new FilterParams { SearchText = "BACKUP_FILTER_TEST" });
+ string backupFile = _testSettingsFile.FullName + ".bak";
+ File.WriteAllText(backupFile, JsonConvert.SerializeObject(goodSettings));
+
+ // Create corrupt main file
+ File.WriteAllText(_testSettingsFile.FullName, "{\"corrupt\": json}");
+
+ // Act
+ LoadResult loadResult = InvokePrivateInstanceMethod("LoadOrCreateNew", _testSettingsFile);
+
+ // Assert
+ Assert.That(loadResult, Is.Not.Null);
+ Assert.That(loadResult.Settings, Is.Not.Null);
+ Assert.That(loadResult.LoadedFromBackup, Is.True, "Should indicate loaded from backup");
+ Assert.That(loadResult.RecoveryMessage, Is.Not.Null.And.Not.Empty, "Should provide recovery message");
+ Assert.That(loadResult.RecoveryTitle, Is.Not.Null.And.Not.Empty, "Should provide recovery title");
+
+ // Verify backup recovery worked - should have BACKUP_FILTER_TEST
+ Assert.That(loadResult.Settings.FilterList.Count, Is.EqualTo(1));
+ Assert.That(loadResult.Settings.FilterList[0].SearchText, Is.EqualTo("BACKUP_FILTER_TEST"));
+
+ // Verify corrupt file preserved
+ string corruptFile = _testSettingsFile.FullName + ".corrupt";
+ Assert.That(File.Exists(corruptFile), Is.True, "Corrupt file should be preserved");
+ }
+
+ [Test]
+ [Category("DeserializationRecovery")]
+ [Description("LoadOrCreateNew should handle corrupt JSON with invalid syntax")]
+ public void LoadOrCreateNew_InvalidJSON_HandlesGracefully ()
+ {
+ // Arrange
+ File.WriteAllText(_testSettingsFile.FullName, "{invalid json syntax");
+
+ // Act
+ LoadResult loadResult = InvokePrivateInstanceMethod("LoadOrCreateNew", _testSettingsFile);
+
+ // Assert
+ Assert.That(loadResult, Is.Not.Null);
+ Assert.That(loadResult.Settings, Is.Not.Null, "Should return valid settings object");
+ // Without backup, will return CriticalFailure or create new settings
+ Assert.That(loadResult.Settings.Preferences, Is.Not.Null);
+ }
+
+ #endregion
+
+ #region Phase 4: Integration Tests
+
+ [Test]
+ [Category("Integration")]
+ [Description("End-to-end save and load should preserve all settings")]
+ public void SaveAndLoad_PreservesAllSettings ()
+ {
+ // Arrange
+ Settings originalSettings = CreateTestSettings();
+ originalSettings.AlwaysOnTop = true;
+ originalSettings.FilterList.Add(new FilterParams { SearchText = "FILTER1" });
+ originalSettings.FilterList.Add(new FilterParams { SearchText = "FILTER2" });
+ originalSettings.SearchHistoryList.Add("SEARCH1");
+ originalSettings.SearchHistoryList.Add("SEARCH2");
+ originalSettings.Preferences.HighlightGroupList.Add(new HighlightGroup { GroupName = "GROUP1" });
+
+ // Act - Save
+ InvokePrivateInstanceMethod("SaveAsJSON", _testSettingsFile, originalSettings);
+
+ // Act - Load
+ LoadResult loadResult = InvokePrivateInstanceMethod("LoadOrCreateNew", _testSettingsFile);
+
+ // Assert
+ Assert.That(loadResult, Is.Not.Null);
+ Assert.That(loadResult.Settings, Is.Not.Null);
+ Assert.That(loadResult.Settings.AlwaysOnTop, Is.EqualTo(originalSettings.AlwaysOnTop));
+ Assert.That(loadResult.Settings.FilterList.Count, Is.EqualTo(2));
+ Assert.That(loadResult.Settings.SearchHistoryList.Count, Is.EqualTo(2));
+ Assert.That(loadResult.Settings.Preferences.HighlightGroupList.Count, Is.EqualTo(1));
+ }
+
+ [Test]
+ [Category("Integration")]
+ [Description("Multiple save operations should maintain backup chain correctly")]
+ public void MultipleSaves_MaintainsBackupChain ()
+ {
+ // Arrange & Act - Save 1
+ Settings settings1 = CreateTestSettings();
+ settings1.AlwaysOnTop = true;
+ InvokePrivateInstanceMethod("SaveAsJSON", _testSettingsFile, settings1);
+
+ // Act - Save 2
+ Settings settings2 = CreateTestSettings();
+ settings2.AlwaysOnTop = false;
+ settings2.FilterList.Add(new FilterParams { SearchText = "FILTER1" });
+ InvokePrivateInstanceMethod("SaveAsJSON", _testSettingsFile, settings2);
+
+ // Act - Save 3
+ Settings settings3 = CreateTestSettings();
+ settings3.AlwaysOnTop = true;
+ settings3.FilterList.Add(new FilterParams { SearchText = "FILTER1" });
+ settings3.FilterList.Add(new FilterParams { SearchText = "FILTER2" });
+ InvokePrivateInstanceMethod("SaveAsJSON", _testSettingsFile, settings3);
+
+ // Assert - Main file has latest
+ string mainContent = File.ReadAllText(_testSettingsFile.FullName);
+ Assert.That(mainContent, Does.Contain("FILTER2"), "Main file should have latest settings");
+
+ // Assert - Backup has previous version
+ string backupFile = _testSettingsFile.FullName + ".bak";
+ Assert.That(File.Exists(backupFile), Is.True, "Backup file should exist");
+
+ string backupContent = File.ReadAllText(backupFile);
+ Assert.That(backupContent, Does.Contain("FILTER1"), "Backup should have previous version");
+ Assert.That(backupContent, Does.Not.Contain("FILTER2"), "Backup should not have latest changes");
+ }
+
+ [Test]
+ [Category("Integration")]
+ [Description("Save operation should be atomic - file always in valid state")]
+ public void Save_IsAtomic_FileAlwaysValid ()
+ {
+ // Arrange
+ Settings settings1 = CreateTestSettings();
+ settings1.FilterList.Add(new FilterParams { SearchText = "INITIAL" });
+ InvokePrivateInstanceMethod("SaveAsJSON", _testSettingsFile, settings1);
+
+ // Act - Save multiple times rapidly
+ for (int i = 0; i < 10; i++)
+ {
+ Settings settings = CreateTestSettings();
+ settings.FilterList.Add(new FilterParams { SearchText = $"FILTER_{i}" });
+ InvokePrivateInstanceMethod("SaveAsJSON", _testSettingsFile, settings);
+
+ // Verify file is always readable
+ Assert.That(_testSettingsFile.Exists, Is.True, $"File should exist after save {i}");
+ Assert.DoesNotThrow(() =>
+ {
+ string json = File.ReadAllText(_testSettingsFile.FullName);
+ Settings? loaded = JsonConvert.DeserializeObject(json);
+ Assert.That(loaded, Is.Not.Null);
+ }, $"File should always be valid JSON after save {i}");
+ }
+ }
+
+ [Test]
+ [Category("Integration")]
+ [Description("Backup file should always be valid when it exists")]
+ public void BackupFile_AlwaysValid_WhenExists ()
+ {
+ // Arrange & Act
+ for (int i = 0; i < 5; i++)
+ {
+ Settings settings = CreateTestSettings();
+ settings.FilterList.Add(new FilterParams { SearchText = $"FILTER_{i}" });
+ InvokePrivateInstanceMethod("SaveAsJSON", _testSettingsFile, settings);
+
+ // Check backup if it exists
+ string backupFile = _testSettingsFile.FullName + ".bak";
+ if (File.Exists(backupFile))
+ {
+ Assert.DoesNotThrow(() =>
+ {
+ string json = File.ReadAllText(backupFile);
+ Settings? loaded = JsonConvert.DeserializeObject(json);
+ Assert.That(loaded, Is.Not.Null, $"Backup should be valid JSON after save {i}");
+ }, $"Backup file should always be valid when it exists (iteration {i})");
+ }
+ }
+ }
+
+ #endregion
+}
diff --git a/src/LogExpert.UI/Controls/DateTimeDragControl.cs b/src/LogExpert.UI/Controls/DateTimeDragControl.cs
index bcc2c796..5856c746 100644
--- a/src/LogExpert.UI/Controls/DateTimeDragControl.cs
+++ b/src/LogExpert.UI/Controls/DateTimeDragControl.cs
@@ -29,7 +29,7 @@ internal partial class DateTimeDragControl : UserControl
private readonly StringFormat _digitsFormat = new();
private int _draggedDigit;
- private DragOrientationsEnum _dragOrientation = DragOrientationsEnum.Vertical;
+ private DragOrientations _dragOrientation = DragOrientations.Vertical;
private readonly ToolStripItem toolStripItemHorizontalDrag = new ToolStripMenuItem();
private readonly ToolStripItem toolStripItemVerticalDrag = new ToolStripMenuItem();
@@ -87,7 +87,7 @@ public DateTimeDragControl ()
public DateTime MaxDateTime { get; set; } = DateTime.MaxValue;
- public DragOrientationsEnum DragOrientation
+ public DragOrientations DragOrientation
{
get => _dragOrientation;
set
@@ -319,9 +319,9 @@ private void BuildContextualMenu ()
private void UpdateContextMenu ()
{
- toolStripItemHorizontalDrag.Enabled = DragOrientation != DragOrientationsEnum.Horizontal;
- toolStripItemVerticalDrag.Enabled = DragOrientation != DragOrientationsEnum.Vertical;
- toolStripItemVerticalInvertedDrag.Enabled = DragOrientation != DragOrientationsEnum.InvertedVertical;
+ toolStripItemHorizontalDrag.Enabled = DragOrientation != DragOrientations.Horizontal;
+ toolStripItemVerticalDrag.Enabled = DragOrientation != DragOrientations.Vertical;
+ toolStripItemVerticalInvertedDrag.Enabled = DragOrientation != DragOrientations.InvertedVertical;
}
private void OnContextMenuStripOpening (object sender, CancelEventArgs e)
@@ -334,7 +334,7 @@ private void OnContextMenuStripOpening (object sender, CancelEventArgs e)
private void OnToolStripItemHorizontalDragClick (object sender, EventArgs e)
{
- DragOrientation = DragOrientationsEnum.Horizontal;
+ DragOrientation = DragOrientations.Horizontal;
toolStripItemHorizontalDrag.Enabled = false;
toolStripItemVerticalDrag.Enabled = true;
toolStripItemVerticalInvertedDrag.Enabled = true;
@@ -342,7 +342,7 @@ private void OnToolStripItemHorizontalDragClick (object sender, EventArgs e)
private void OnToolStripItemVerticalDragClick (object sender, EventArgs e)
{
- DragOrientation = DragOrientationsEnum.Vertical;
+ DragOrientation = DragOrientations.Vertical;
toolStripItemHorizontalDrag.Enabled = true;
toolStripItemVerticalDrag.Enabled = false;
toolStripItemVerticalInvertedDrag.Enabled = true;
@@ -350,7 +350,7 @@ private void OnToolStripItemVerticalDragClick (object sender, EventArgs e)
private void OnToolStripItemVerticalInvertedDragClick (object sender, EventArgs e)
{
- DragOrientation = DragOrientationsEnum.InvertedVertical;
+ DragOrientation = DragOrientations.InvertedVertical;
toolStripItemHorizontalDrag.Enabled = true;
toolStripItemVerticalDrag.Enabled = true;
toolStripItemVerticalInvertedDrag.Enabled = false;
@@ -466,12 +466,12 @@ protected override void OnMouseMove (MouseEventArgs e)
int diff;
switch (DragOrientation)
{
- case DragOrientationsEnum.Vertical:
+ case DragOrientations.Vertical:
{
diff = _startMouseY - e.Y;
break;
}
- case DragOrientationsEnum.InvertedVertical:
+ case DragOrientations.InvertedVertical:
{
diff = _startMouseY + e.Y;
break;
diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs
index 0918752b..c99a075e 100644
--- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs
+++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs
@@ -2765,6 +2765,7 @@ private void OnSaveProjectToolStripMenuItemClick (object sender, EventArgs e)
FileNames = fileNames,
TabLayoutXml = SaveLayout()
};
+
ProjectPersister.SaveProjectData(fileName, projectData);
}
}
diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.designer.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.designer.cs
index 2b322b27..59eb0f7d 100644
--- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.designer.cs
+++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.designer.cs
@@ -1126,7 +1126,7 @@ private void InitializeComponent()
dragControlDateTime.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
dragControlDateTime.BackColor = System.Drawing.SystemColors.Control;
dragControlDateTime.DateTime = new System.DateTime(0L);
- dragControlDateTime.DragOrientation = DragOrientationsEnum.Vertical;
+ dragControlDateTime.DragOrientation = DragOrientations.Vertical;
dragControlDateTime.Font = new System.Drawing.Font("Courier New", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0);
dragControlDateTime.ForeColor = System.Drawing.SystemColors.ControlDarkDark;
dragControlDateTime.HoverColor = System.Drawing.Color.LightGray;
diff --git a/src/LogExpert.UI/Dialogs/SettingsDialog.cs b/src/LogExpert.UI/Dialogs/SettingsDialog.cs
index 5a76f8c0..def435ed 100644
--- a/src/LogExpert.UI/Dialogs/SettingsDialog.cs
+++ b/src/LogExpert.UI/Dialogs/SettingsDialog.cs
@@ -13,7 +13,7 @@
namespace LogExpert.Dialogs;
-//TODO: This class should not knoow ConfigManager?
+//TODO: This class should not know ConfigManager?
[SupportedOSPlatform("windows")]
internal partial class SettingsDialog : Form
{
@@ -80,9 +80,9 @@ private void FillDialog ()
checkBoxFilterTail.Checked = Preferences.FilterTail;
checkBoxFollowTail.Checked = Preferences.FollowTail;
- radioButtonHorizMouseDrag.Checked = Preferences.TimestampControlDragOrientation == DragOrientationsEnum.Horizontal;
- radioButtonVerticalMouseDrag.Checked = Preferences.TimestampControlDragOrientation == DragOrientationsEnum.Vertical;
- radioButtonVerticalMouseDragInverted.Checked = Preferences.TimestampControlDragOrientation == DragOrientationsEnum.InvertedVertical;
+ radioButtonHorizMouseDrag.Checked = Preferences.TimestampControlDragOrientation == DragOrientations.Horizontal;
+ radioButtonVerticalMouseDrag.Checked = Preferences.TimestampControlDragOrientation == DragOrientations.Vertical;
+ radioButtonVerticalMouseDragInverted.Checked = Preferences.TimestampControlDragOrientation == DragOrientations.InvertedVertical;
checkBoxSingleInstance.Checked = Preferences.AllowOnlyOneInstance;
checkBoxOpenLastFiles.Checked = Preferences.OpenLastFiles;
@@ -212,10 +212,12 @@ private void SaveMultifileData ()
private void OnBtnToolClickInternal (TextBox textBox)
{
- OpenFileDialog dlg = new();
- dlg.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
+ OpenFileDialog dlg = new()
+ {
+ InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)
+ };
- if (string.IsNullOrEmpty(textBox.Text) == false)
+ if (!string.IsNullOrEmpty(textBox.Text))
{
FileInfo info = new(textBox.Text);
if (info.Directory != null && info.Directory.Exists)
@@ -309,7 +311,7 @@ private void FillColumnizerList ()
foreach (var columnizer in columnizers)
{
- comboColumn.Items.Add(columnizer.GetName());
+ _ = comboColumn.Items.Add(columnizer.GetName());
}
//comboColumn.DisplayMember = "Name";
//comboColumn.ValueMember = "Columnizer";
@@ -317,21 +319,21 @@ private void FillColumnizerList ()
foreach (var maskEntry in Preferences.ColumnizerMaskList)
{
DataGridViewRow row = new();
- row.Cells.Add(new DataGridViewTextBoxCell());
+ _ = row.Cells.Add(new DataGridViewTextBoxCell());
DataGridViewComboBoxCell cell = new();
foreach (var logColumnizer in columnizers)
{
- cell.Items.Add(logColumnizer.GetName());
+ _ = cell.Items.Add(logColumnizer.GetName());
}
- row.Cells.Add(cell);
+ _ = row.Cells.Add(cell);
row.Cells[0].Value = maskEntry.Mask;
var columnizer = ColumnizerPicker.DecideColumnizerByName(maskEntry.ColumnizerName,
PluginRegistry.PluginRegistry.Instance.RegisteredColumnizers);
row.Cells[1].Value = columnizer.GetName();
- dataGridViewColumnizer.Rows.Add(row);
+ _ = dataGridViewColumnizer.Rows.Add(row);
}
var count = dataGridViewColumnizer.RowCount;
@@ -355,21 +357,21 @@ private void FillHighlightMaskList ()
foreach (var group in (IList)_logTabWin.HighlightGroupList)
{
- comboColumn.Items.Add(group.GroupName);
+ _ = comboColumn.Items.Add(group.GroupName);
}
foreach (var maskEntry in Preferences.HighlightMaskList)
{
DataGridViewRow row = new();
- row.Cells.Add(new DataGridViewTextBoxCell());
+ _ = row.Cells.Add(new DataGridViewTextBoxCell());
DataGridViewComboBoxCell cell = new();
foreach (var group in (IList)_logTabWin.HighlightGroupList)
{
- cell.Items.Add(group.GroupName);
+ _ = cell.Items.Add(group.GroupName);
}
- row.Cells.Add(cell);
+ _ = row.Cells.Add(cell);
row.Cells[0].Value = maskEntry.Mask;
var currentGroup = _logTabWin.FindHighlightGroup(maskEntry.HighlightGroupName);
@@ -377,7 +379,7 @@ private void FillHighlightMaskList ()
currentGroup ??= highlightGroupList.Count > 0 ? highlightGroupList[0] : new HighlightGroup();
row.Cells[1].Value = currentGroup.GroupName;
- dataGridViewHighlightMask.Rows.Add(row);
+ _ = dataGridViewHighlightMask.Rows.Add(row);
}
var count = dataGridViewHighlightMask.RowCount;
@@ -428,7 +430,7 @@ private void FillPluginList ()
foreach (var entry in PluginRegistry.PluginRegistry.Instance.RegisteredContextMenuPlugins)
{
- listBoxPlugin.Items.Add(entry);
+ _ = listBoxPlugin.Items.Add(entry);
if (entry is ILogExpertPluginConfigurator configurator)
{
configurator.StartConfig();
@@ -437,7 +439,7 @@ private void FillPluginList ()
foreach (var entry in PluginRegistry.PluginRegistry.Instance.RegisteredKeywordActions)
{
- listBoxPlugin.Items.Add(entry);
+ _ = listBoxPlugin.Items.Add(entry);
if (entry is ILogExpertPluginConfigurator configurator)
{
configurator.StartConfig();
@@ -446,7 +448,7 @@ private void FillPluginList ()
foreach (var entry in PluginRegistry.PluginRegistry.Instance.RegisteredFileSystemPlugins)
{
- listBoxPlugin.Items.Add(entry);
+ _ = listBoxPlugin.Items.Add(entry);
if (entry is ILogExpertPluginConfigurator configurator)
{
configurator.StartConfig();
@@ -483,7 +485,7 @@ private void FillToolListbox ()
foreach (var tool in Preferences.ToolEntries)
{
- listBoxTools.Items.Add(tool.Clone(), tool.IsFavourite);
+ _ = listBoxTools.Items.Add(tool.Clone(), tool.IsFavourite);
}
if (listBoxTools.Items.Count > 0)
@@ -565,7 +567,7 @@ private void DisplayCurrentIcon ()
{
Image image = icon.ToBitmap();
buttonIcon.Image = image;
- NativeMethods.DestroyIcon(icon.Handle);
+ _ = NativeMethods.DestroyIcon(icon.Handle);
icon.Dispose();
}
else
@@ -579,12 +581,12 @@ private void FillEncodingList ()
{
comboBoxEncoding.Items.Clear();
- comboBoxEncoding.Items.Add(Encoding.ASCII);
- comboBoxEncoding.Items.Add(Encoding.Default);
- comboBoxEncoding.Items.Add(Encoding.GetEncoding("iso-8859-1"));
- comboBoxEncoding.Items.Add(Encoding.UTF8);
- comboBoxEncoding.Items.Add(Encoding.Unicode);
- comboBoxEncoding.Items.Add(CodePagesEncodingProvider.Instance.GetEncoding(1252));
+ _ = comboBoxEncoding.Items.Add(Encoding.ASCII);
+ _ = comboBoxEncoding.Items.Add(Encoding.Default);
+ _ = comboBoxEncoding.Items.Add(Encoding.GetEncoding("iso-8859-1"));
+ _ = comboBoxEncoding.Items.Add(Encoding.UTF8);
+ _ = comboBoxEncoding.Items.Add(Encoding.Unicode);
+ _ = comboBoxEncoding.Items.Add(CodePagesEncodingProvider.Instance.GetEncoding(1252));
comboBoxEncoding.ValueMember = "HeaderName";
}
@@ -624,18 +626,11 @@ private void OnBtnOkClick (object sender, EventArgs e)
Preferences.FilterTail = checkBoxFilterTail.Checked;
Preferences.FollowTail = checkBoxFollowTail.Checked;
- if (radioButtonVerticalMouseDrag.Checked)
- {
- Preferences.TimestampControlDragOrientation = DragOrientationsEnum.Vertical;
- }
- else if (radioButtonVerticalMouseDragInverted.Checked)
- {
- Preferences.TimestampControlDragOrientation = DragOrientationsEnum.InvertedVertical;
- }
- else
- {
- Preferences.TimestampControlDragOrientation = DragOrientationsEnum.Horizontal;
- }
+ Preferences.TimestampControlDragOrientation = radioButtonVerticalMouseDrag.Checked
+ ? DragOrientations.Vertical
+ : radioButtonVerticalMouseDragInverted.Checked
+ ? DragOrientations.InvertedVertical
+ : DragOrientations.Horizontal;
SaveColumnizerList();
@@ -708,7 +703,7 @@ private void OnDataGridViewColumnizerRowsAdded (object sender, DataGridViewRowsA
var comboCell = (DataGridViewComboBoxCell)dataGridViewColumnizer.Rows[e.RowIndex].Cells[1];
if (comboCell.Items.Count > 0)
{
- // comboCell.Value = comboCell.Items[0];
+ //comboCell.Value = comboCell.Items[0];
}
}
@@ -717,7 +712,7 @@ private void OnBtnDeleteClick (object sender, EventArgs e)
if (dataGridViewColumnizer.CurrentRow != null && !dataGridViewColumnizer.CurrentRow.IsNewRow)
{
var index = dataGridViewColumnizer.CurrentRow.Index;
- dataGridViewColumnizer.EndEdit();
+ _ = dataGridViewColumnizer.EndEdit();
dataGridViewColumnizer.Rows.RemoveAt(index);
}
}
@@ -767,14 +762,14 @@ private void OnListBoxPluginSelectedIndexChanged (object sender, EventArgs e)
{
_selectedPlugin?.HideConfigForm();
- var o = listBoxPlugin.SelectedItem;
+ var selectedPlugin = listBoxPlugin.SelectedItem;
- if (o != null)
+ if (selectedPlugin != null)
{
- _selectedPlugin = o as ILogExpertPluginConfigurator;
-
- if (o is ILogExpertPluginConfigurator)
+ if (selectedPlugin is ILogExpertPluginConfigurator pluginConfigurator)
{
+ _selectedPlugin = pluginConfigurator;
+
if (_selectedPlugin.HasEmbeddedForm())
{
buttonConfigPlugin.Enabled = false;
@@ -823,7 +818,7 @@ private void OnPortableModeCheckedChanged (object sender, EventArgs e)
{
if (Directory.Exists(ConfigManager.PortableModeDir) == false)
{
- Directory.CreateDirectory(ConfigManager.PortableModeDir);
+ _ = Directory.CreateDirectory(ConfigManager.PortableModeDir);
}
using (File.Create(ConfigManager.PortableModeDir + Path.DirectorySeparatorChar + ConfigManager.PortableModeSettingsFileName))
@@ -857,9 +852,8 @@ private void OnPortableModeCheckedChanged (object sender, EventArgs e)
}
catch (Exception exception)
{
- MessageBox.Show($@"Could not create / delete marker for Portable Mode: {exception}", @"Error", MessageBoxButtons.OK);
+ _ = MessageBox.Show($@"Could not create / delete marker for Portable Mode: {exception}", @"Error", MessageBoxButtons.OK);
}
-
}
private void OnBtnConfigPluginClick (object sender, EventArgs e)
@@ -894,7 +888,9 @@ private void OnBtnToolUpClick (object sender, EventArgs e)
var isChecked = listBoxTools.GetItemChecked(i);
var item = listBoxTools.Items[i];
listBoxTools.Items.RemoveAt(i);
+
i--;
+
listBoxTools.Items.Insert(i, item);
listBoxTools.SelectedIndex = i;
listBoxTools.SetItemChecked(i, isChecked);
@@ -910,7 +906,9 @@ private void OnBtnToolDownClick (object sender, EventArgs e)
var isChecked = listBoxTools.GetItemChecked(i);
var item = listBoxTools.Items[i];
listBoxTools.Items.RemoveAt(i);
+
i++;
+
listBoxTools.Items.Insert(i, item);
listBoxTools.SelectedIndex = i;
listBoxTools.SetItemChecked(i, isChecked);
@@ -920,7 +918,7 @@ private void OnBtnToolDownClick (object sender, EventArgs e)
[SupportedOSPlatform("windows")]
private void OnBtnToolAddClick (object sender, EventArgs e)
{
- listBoxTools.Items.Add(new ToolEntry());
+ _ = listBoxTools.Items.Add(new ToolEntry());
listBoxTools.SelectedIndex = listBoxTools.Items.Count - 1;
}
@@ -1007,7 +1005,7 @@ private void OnBtnExportClick (object sender, EventArgs e)
}
///
- ///
+ /// Import settings from file
///
///
///
@@ -1029,14 +1027,51 @@ private void OnBtnImportClick (object sender, EventArgs e)
}
catch (Exception ex)
{
- MessageBox.Show(this, $@"Settings could not be imported: {ex}", @"LogExpert");
+ _ = MessageBox.Show(this, $@"Settings could not be imported: {ex}", @"LogExpert");
return;
}
- ConfigManager.Import(fileInfo, dlg.ImportFlags);
+ ImportResult importResult = ConfigManager.Import(fileInfo, dlg.ImportFlags);
+
+ if (!importResult.Success)
+ {
+ if (importResult.RequiresUserConfirmation)
+ {
+ var confirmResult = MessageBox.Show(
+ this,
+ importResult.ConfirmationMessage,
+ importResult.ConfirmationTitle,
+ MessageBoxButtons.YesNo,
+ MessageBoxIcon.Warning,
+ MessageBoxDefaultButton.Button2);
+
+ if (confirmResult == DialogResult.Yes)
+ {
+ // User confirmed, retry import without validation
+ _ = ConfigManager.Import(fileInfo, dlg.ImportFlags);
+ }
+ else
+ {
+ return;
+ }
+ }
+ else
+ {
+ _ = MessageBox.Show(
+ this,
+ importResult.ErrorMessage,
+ importResult.ErrorTitle,
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Error);
+
+ return;
+ }
+ }
+
Preferences = ConfigManager.Settings.Preferences;
FillDialog();
- MessageBox.Show(this, @"Settings imported", @"LogExpert");
+
+ _ = MessageBox.Show(this, @"Settings imported", @"LogExpert");
}
}
diff --git a/src/LogExpert/Config/ConfigManager.cs b/src/LogExpert/Config/ConfigManager.cs
index f9277f88..8f67146f 100644
--- a/src/LogExpert/Config/ConfigManager.cs
+++ b/src/LogExpert/Config/ConfigManager.cs
@@ -1,10 +1,12 @@
using System.Drawing;
using System.Globalization;
using System.Reflection;
-using System.Text;
+using System.Security;
using System.Windows.Forms;
using LogExpert.Core.Classes;
+using LogExpert.Core.Classes.Filter;
+using LogExpert.Core.Classes.JsonConverters;
using LogExpert.Core.Config;
using LogExpert.Core.Entities;
using LogExpert.Core.EventArguments;
@@ -20,13 +22,26 @@ public class ConfigManager : IConfigManager
{
#region Fields
- private static readonly ILogger _logger = LogManager.GetCurrentClassLogger();
+ private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
private static readonly object _monitor = new();
private static ConfigManager _instance;
private readonly object _loadSaveLock = new();
private Settings _settings;
+ private static readonly JsonSerializerSettings _jsonSettings = new()
+ {
+ Converters =
+ {
+ new ColumnizerJsonConverter(),
+ new EncodingJsonConverter()
+ },
+ Formatting = Formatting.Indented,
+ //This is needed for the BookmarkList and the Bookmark Overlay
+ ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
+ PreserveReferencesHandling = PreserveReferencesHandling.Objects,
+ };
+
#endregion
#region cTor
@@ -76,7 +91,7 @@ public static ConfigManager Instance
IConfigManager IConfigManager.Instance => Instance;
- // Action