diff --git a/Common/AppSettings/AppSetting.cs b/Common/AppSettings/AppSetting.cs new file mode 100644 index 0000000..1ae2af8 --- /dev/null +++ b/Common/AppSettings/AppSetting.cs @@ -0,0 +1,24 @@ +namespace Common.AppSettings; + +[Serializable] +public class AppSetting +{ + public string? Key { get; set; } + public string? Value { get; set; } + + /// + /// Parameterless constructor for serialization. + /// + public AppSetting() + { + } + + /// + /// Constructor with Initialization. + /// + public AppSetting(string key, string value) + { + this.Key = key; + this.Value = value; + } +} diff --git a/Common/AppSettings/IAppSettingsProvider.cs b/Common/AppSettings/IAppSettingsProvider.cs new file mode 100644 index 0000000..dda2e20 --- /dev/null +++ b/Common/AppSettings/IAppSettingsProvider.cs @@ -0,0 +1,29 @@ +using Common.Logging; + +namespace Common.AppSettings; + +public interface IAppSettingsProvider +{ + /// + /// Any internal event inside of the App Settings Provider. + /// + event EventHandler? LogWorthyEvent; + + /// + /// Retrieves the app settings from the persistant location and loads them into memory. + /// Loading settings will override the currently stored settings in the memory. + /// + void Load(); + + /// + /// Saves the app settings to the persistant location. + /// + void Save(); + + + bool WriteSetting(string key, string value); + + bool TryReadSetting(string key, out string value); + + bool TryReadSettingOrAddDefault(string key, out string value, string defaultValue); +} diff --git a/Common/AppSettings/XmlAppSettingsProvider.cs b/Common/AppSettings/XmlAppSettingsProvider.cs new file mode 100644 index 0000000..a5e6fc0 --- /dev/null +++ b/Common/AppSettings/XmlAppSettingsProvider.cs @@ -0,0 +1,186 @@ +using Common.Logging; +using System.Xml.Serialization; + +namespace Common.AppSettings; + +public class XmlAppSettingsProvider : IAppSettingsProvider +{ + private readonly string settingsFilePath; + private readonly XmlSerializer serializer; + private List settings; + + public event EventHandler? LogWorthyEvent; + + public XmlAppSettingsProvider(string settingsFilePath) + { + // check for file path validity + // TODO create file if it does not exist yet + if (String.IsNullOrEmpty(settingsFilePath)) { throw new ArgumentNullException(nameof(settingsFilePath)); } + + this.settingsFilePath = settingsFilePath; + this.settings = new List(); + this.serializer = new XmlSerializer(typeof(List)); + } + + public void Load() + { + // read file by creating a stream. this will throw an exception when the file does not exist. + using var fileStream = new FileStream(this.settingsFilePath, FileMode.Open); + + // deserialize and cast to List if not null + var deserializedObj = this.serializer.Deserialize(fileStream); + if(deserializedObj != null) + { + // create log entry if there were already some settings loaded + if(this.settings.Count > 0) + { + this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Warn, category: nameof(XmlAppSettingsProvider), + message: $"'{nameof(Load)}'() deserialization overwrote previously loaded settings.")); + } + this.settings = (List)deserializedObj; + } + else + { + this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Warn, category: nameof(XmlAppSettingsProvider), + message: $"'{nameof(Load)}'() deserialization resulted in null object. Did not change loaded settings.")); + } + } + + public void Save() + { + // TODO write log entries for when something goes wrong + TextWriter writer = new StreamWriter(this.settingsFilePath); + this.serializer.Serialize(writer, this.settings); + writer.Close(); + } + + + + public bool WriteSetting(string key, string value) + { + // guard empty key or value + if(String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); + if (String.IsNullOrEmpty(value)) throw new ArgumentNullException(nameof(value)); + + // get all settings with this key + var foundSettings = this.settings.FindAll(x => x.Key == key); + + // check if the key already exists multiple times + if(foundSettings.Count > 1) + { + this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Error, category: nameof(XmlAppSettingsProvider), + message: $"'{nameof(WriteSetting)}'() cancelled because {foundSettings.Count} settings with the key '{key}' were found. Expected none or exactly one.")); + + return false; // cancel + } + // if exactly one is found => remove existing app setting + else if(foundSettings.Count == 1) + { + this.settings.Remove(this.settings.Find(x => x.Key == key)!); + } + // none existing yet => inform via event + else if(foundSettings.Count == 0) + { + this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Info, category: nameof(XmlAppSettingsProvider), + message: $"'{nameof(WriteSetting)}'() initialized new setting with key '{key}' and value '{value}'.")); + } + + // add new setting + this.settings.Add(new AppSetting(key, value)); + return true; + } + + public bool TryReadSetting(string key, out string value) + { + // guard empty key + if (String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); + + // get all settings with this key + var foundSettings = this.settings.FindAll(x => x.Key == key); + + // too many values found + if (foundSettings.Count > 1) + { + this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Error, category: nameof(XmlAppSettingsProvider), + message: $"'{nameof(TryReadSetting)}'() did find {foundSettings.Count} settings with the key '{key}'. Expected only one.")); + + value = string.Empty; + return false; + } + + // no value found + if (foundSettings.Count == 0) + { + this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Error, category: nameof(XmlAppSettingsProvider), + message: $"'{nameof(TryReadSetting)}'() did not find a setting with the key '{key}'.")); + + value = string.Empty; + return false; + } + + // extract correct setting when found exactly one + var setting = foundSettings[0]; + if(setting.Value != null) + { + value = setting.Value; + } + // if value is null it was probably an empty string or similar => set it to empty string + else + { + this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Warn, category: nameof(XmlAppSettingsProvider), + message: $"'{nameof(TryReadSetting)}'() value of retrieved setting with key '{key}' was null. Returned empty string instead.")); + + value = string.Empty; + } + + return true; + } + + public bool TryReadSettingOrAddDefault(string key, out string value, string defaultValue) + { + // guard empty key or defaultValue + if (String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); + if (String.IsNullOrEmpty(defaultValue)) throw new ArgumentNullException(nameof(defaultValue)); + + // get all settings with this key + var foundSettings = this.settings.FindAll(x => x.Key == key); + + // too many values found + if (foundSettings.Count > 1) + { + this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Error, category: nameof(XmlAppSettingsProvider), + message: $"'{nameof(TryReadSettingOrAddDefault)}'() did find {foundSettings.Count} settings with the key '{key}'. Expected only one.")); + + value = string.Empty; + return false; + } + + // no value found => initialize with the default value + if (foundSettings.Count == 0) + { + this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Info, category: nameof(XmlAppSettingsProvider), + message: $"'{nameof(TryReadSettingOrAddDefault)}'() did not find a setting with the key '{key}'. Adding setting with value '{defaultValue}'.")); + + this.WriteSetting(key, defaultValue); + value = defaultValue; + return true; + } + + // extract correct setting when found exactly one + var setting = foundSettings[0]; + if (setting.Value != null) + { + value = setting.Value; + } + // if value is null it was probably an empty string or similar => set it to empty string + else + { + this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Warn, category: nameof(XmlAppSettingsProvider), + message: $"'{nameof(TryReadSetting)}'() value of retrieved setting with key '{key}' was null. Returned empty string instead.")); + + value = string.Empty; + } + + return true; + } +} diff --git a/Common/Helpers/EnumHelpers.cs b/Common/Helpers/EnumHelpers.cs new file mode 100644 index 0000000..0961b0d --- /dev/null +++ b/Common/Helpers/EnumHelpers.cs @@ -0,0 +1,15 @@ +namespace Common.Helpers; + +public static class EnumHelpers +{ + /// + /// Parse enum from string to enum type. + /// + /// type of the enum + /// value to parse to enum + /// returns enum object with value + public static T ParseEnum(string value) + { + return (T)Enum.Parse(typeof(T), value, true); + } +} diff --git a/Common/Logging/ILogger.cs b/Common/Logging/ILogger.cs index 7469089..347c486 100644 --- a/Common/Logging/ILogger.cs +++ b/Common/Logging/ILogger.cs @@ -43,6 +43,12 @@ public interface ILogger void SetMinimumLogLevel(LogLevel newMinimumLogLevel); #region Logging methods + /// + /// Create a Log entry with a complete object. + /// + /// to log + void Log(LogEntry logEntry); + /// /// Create a Log entry with the level . /// diff --git a/Common/Logging/SerilogLogger.cs b/Common/Logging/SerilogLogger.cs index 7acb1ed..bc416d2 100644 --- a/Common/Logging/SerilogLogger.cs +++ b/Common/Logging/SerilogLogger.cs @@ -1,19 +1,20 @@ -using Common.Logging; -using Serilog; +using Serilog; using Serilog.Core; using Serilog.Events; -namespace Common.Logger; +// alias to prevent conflict own Log method +using SerilogLoggingInstance = Serilog.Log; + +namespace Common.Logging; /// /// Implements a Logger that uses the Serilog package to create log entries and distributes them into different sinks. /// Implemented Sinks: File (rolling file), Debug (). /// -public class SerilogLogger : Logging.ILogger +public class SerilogLogger : ILogger { private readonly LoggingLevelSwitch loggingLevelSwitch; - public event EventHandler? NewLogEntry; public LogLevel CurrentMinimumLogLevel { get; private set; } = LogLevel.Undefined; @@ -38,7 +39,7 @@ public class SerilogLogger : Logging.ILogger // create logger instance if (logToDebug) { - Log.Logger = new LoggerConfiguration() + SerilogLoggingInstance.Logger = new LoggerConfiguration() .WriteTo.Debug() .WriteTo.File(logFilePath, rollingInterval: RollingInterval.Day, @@ -48,7 +49,7 @@ public class SerilogLogger : Logging.ILogger } else { - Log.Logger = new LoggerConfiguration() + SerilogLoggingInstance.Logger = new LoggerConfiguration() .WriteTo.File(logFilePath, rollingInterval: RollingInterval.Day, levelSwitch: loggingLevelSwitch, @@ -87,10 +88,12 @@ public class SerilogLogger : Logging.ILogger public void StopLogging() { - Log.CloseAndFlush(); + SerilogLoggingInstance.CloseAndFlush(); } - private void CreateLogEntry(LogEntry logEntry) + + #region Logging methods + public void Log(LogEntry logEntry) { var serilogEventLevel = this.ConvertGenericToSerilogLogLevel(logEntry.LogLevel); @@ -98,31 +101,31 @@ public class SerilogLogger : Logging.ILogger switch (serilogEventLevel) { case LogEventLevel.Verbose: - Log.Verbose(logEntry.ToString()); + SerilogLoggingInstance.Verbose(logEntry.ToString()); break; case LogEventLevel.Debug: - Log.Debug(logEntry.ToString()); + SerilogLoggingInstance.Debug(logEntry.ToString()); break; case LogEventLevel.Information: - Log.Information(logEntry.ToString()); + SerilogLoggingInstance.Information(logEntry.ToString()); break; case LogEventLevel.Warning: - Log.Warning(logEntry.ToString()); + SerilogLoggingInstance.Warning(logEntry.ToString()); break; case LogEventLevel.Error: - Log.Error(logEntry.ToString()); + SerilogLoggingInstance.Error(logEntry.ToString()); break; case LogEventLevel.Fatal: - Log.Error(logEntry.ToString()); + SerilogLoggingInstance.Error(logEntry.ToString()); break; default: - throw new NotImplementedException($"'{nameof(CreateLogEntry)}()' does not contain an implementation for {nameof(LogEventLevel)} {serilogEventLevel}."); + throw new NotImplementedException($"'{nameof(Log)}()' does not contain an implementation for {nameof(LogEventLevel)} {serilogEventLevel}."); } // Raise event (only if the log is wanted due to the configured log level) @@ -132,41 +135,39 @@ public class SerilogLogger : Logging.ILogger } } - - #region Logging methods public void LogTrace(string message, string category = "") { - this.CreateLogEntry(new LogEntry { Category = category, LogLevel = LogLevel.Trace, Message = message }); + this.Log(new LogEntry { Category = category, LogLevel = LogLevel.Trace, Message = message }); } public void LogDebug(string message, string category = "") { - this.CreateLogEntry(new LogEntry { Category = category, LogLevel = LogLevel.Debug, Message = message }); + this.Log(new LogEntry { Category = category, LogLevel = LogLevel.Debug, Message = message }); } public void LogInfo(string message, string category = "") { - this.CreateLogEntry(new LogEntry { Category = category, LogLevel = LogLevel.Info, Message = message }); + this.Log(new LogEntry { Category = category, LogLevel = LogLevel.Info, Message = message }); } public void LogWarn(string message, string category = "") { - this.CreateLogEntry(new LogEntry { Category = category, LogLevel = LogLevel.Warn, Message = message }); + this.Log(new LogEntry { Category = category, LogLevel = LogLevel.Warn, Message = message }); } public void LogError(string message, string category = "") { - this.CreateLogEntry(new LogEntry { Category = category, LogLevel = LogLevel.Error, Message = message }); + this.Log(new LogEntry { Category = category, LogLevel = LogLevel.Error, Message = message }); } public void LogException(Exception exception, string category = "") { - this.CreateLogEntry(new LogEntry { Category = category, LogLevel = LogLevel.Error, Exception = exception }); + this.Log(new LogEntry { Category = category, LogLevel = LogLevel.Error, Exception = exception }); } public void LogException(Exception exception, string message, string category = "") { - this.CreateLogEntry( + this.Log( new LogEntry { Category = category, diff --git a/MultiTerm.Core/Factories/ITerminalViewModelFactory.cs b/MultiTerm.Core/Factories/ITerminalViewModelFactory.cs index 63b09f6..60888ff 100644 --- a/MultiTerm.Core/Factories/ITerminalViewModelFactory.cs +++ b/MultiTerm.Core/Factories/ITerminalViewModelFactory.cs @@ -1,4 +1,4 @@ -using MultiTerm.Core.Common; +using MultiTerm.Core.Types; using MultiTerm.Core.ViewModel; namespace MultiTerm.Core.Factories diff --git a/MultiTerm.Core/Factories/TerminalViewModelFactory.cs b/MultiTerm.Core/Factories/TerminalViewModelFactory.cs index 3c9566c..367855f 100644 --- a/MultiTerm.Core/Factories/TerminalViewModelFactory.cs +++ b/MultiTerm.Core/Factories/TerminalViewModelFactory.cs @@ -1,4 +1,4 @@ -using MultiTerm.Core.Common; +using MultiTerm.Core.Types; using MultiTerm.Core.ViewModel; namespace MultiTerm.Core.Factories; diff --git a/MultiTerm.Core/Common/NewlineSeparatorType.cs b/MultiTerm.Core/Types/NewlineSeparatorType.cs similarity index 94% rename from MultiTerm.Core/Common/NewlineSeparatorType.cs rename to MultiTerm.Core/Types/NewlineSeparatorType.cs index 30030ab..c5a2687 100644 --- a/MultiTerm.Core/Common/NewlineSeparatorType.cs +++ b/MultiTerm.Core/Types/NewlineSeparatorType.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace MultiTerm.Core.Common; +namespace MultiTerm.Core.Types; public enum NewlineSeparatorType { diff --git a/MultiTerm.Core/Common/ProtocolType.cs b/MultiTerm.Core/Types/ProtocolType.cs similarity index 93% rename from MultiTerm.Core/Common/ProtocolType.cs rename to MultiTerm.Core/Types/ProtocolType.cs index ae241e8..799cd7f 100644 --- a/MultiTerm.Core/Common/ProtocolType.cs +++ b/MultiTerm.Core/Types/ProtocolType.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace MultiTerm.Core.Common; +namespace MultiTerm.Core.Types; public enum ProtocolType { diff --git a/MultiTerm.Core/Common/TerminalViewType.cs b/MultiTerm.Core/Types/TerminalViewType.cs similarity index 89% rename from MultiTerm.Core/Common/TerminalViewType.cs rename to MultiTerm.Core/Types/TerminalViewType.cs index 66445d9..4005741 100644 --- a/MultiTerm.Core/Common/TerminalViewType.cs +++ b/MultiTerm.Core/Types/TerminalViewType.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace MultiTerm.Core.Common; +namespace MultiTerm.Core.Types; public enum TerminalViewType { diff --git a/MultiTerm.Core/ViewModel/ITerminalViewModel.cs b/MultiTerm.Core/ViewModel/ITerminalViewModel.cs index c8b18c3..eedc0a8 100644 --- a/MultiTerm.Core/ViewModel/ITerminalViewModel.cs +++ b/MultiTerm.Core/ViewModel/ITerminalViewModel.cs @@ -1,4 +1,4 @@ -using MultiTerm.Core.Common; +using MultiTerm.Core.Types; namespace MultiTerm.Core.ViewModel; diff --git a/MultiTerm.Core/ViewModel/SendReceiveViewModel.cs b/MultiTerm.Core/ViewModel/SendReceiveViewModel.cs index 0467e90..73998e8 100644 --- a/MultiTerm.Core/ViewModel/SendReceiveViewModel.cs +++ b/MultiTerm.Core/ViewModel/SendReceiveViewModel.cs @@ -1,4 +1,4 @@ -using MultiTerm.Core.Common; +using MultiTerm.Core.Types; namespace MultiTerm.Core.ViewModel; diff --git a/MultiTerm.Core/ViewModel/ShellViewModel.cs b/MultiTerm.Core/ViewModel/ShellViewModel.cs index e0373a5..2e09c1e 100644 --- a/MultiTerm.Core/ViewModel/ShellViewModel.cs +++ b/MultiTerm.Core/ViewModel/ShellViewModel.cs @@ -1,15 +1,19 @@ -using Common.Logging; -using Common.StartupHelpers; +using Common.AppSettings; +using Common.Logging; +using Common.Helpers; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using MultiTerm.Core.Common; using MultiTerm.Core.Factories; using System.Collections.ObjectModel; +using MultiTerm.Core.Types; namespace MultiTerm.Core.ViewModel; public partial class ShellViewModel : ObservableObject { + private const string defaultReceiveNewlineSeparatorAppSettingsKey = "DefaultReceiveNewlineSeparator"; + private const string defaultSendNewlineSeparatorAppSettingsKey = "DefaultSendNewlineSeparator"; + [ObservableProperty] private string title = "ShellView Test"; @@ -37,11 +41,20 @@ public partial class ShellViewModel : ObservableObject private readonly ITerminalViewModelFactory terminalViewModelFactory; private readonly ILogger logger; + private readonly IAppSettingsProvider appSettings; - public ShellViewModel(ITerminalViewModelFactory terminalViewModelFactory, ILogger logger) + public ShellViewModel(ITerminalViewModelFactory terminalViewModelFactory, ILogger logger, IAppSettingsProvider appSettings) { this.terminalViewModelFactory = terminalViewModelFactory; this.logger = logger; + this.appSettings = appSettings; + + // initialize newline separators from persistent settings + this.appSettings.TryReadSettingOrAddDefault(defaultReceiveNewlineSeparatorAppSettingsKey, out string settingValueReceiveNLSep, NewlineSeparatorType.None.ToString()); + this.DefaultReceiveNewlineSeparator = EnumHelpers.ParseEnum(settingValueReceiveNLSep); + this.appSettings.TryReadSettingOrAddDefault(defaultSendNewlineSeparatorAppSettingsKey, out string settingValueSendNLSep, NewlineSeparatorType.None.ToString()); + this.DefaultSendNewlineSeparator = EnumHelpers.ParseEnum(settingValueSendNLSep); + // TEMP Init this.AppendTerminalWithSelectedViewType(ProtocolType.Serial); } diff --git a/MultiTerm.Core/ViewModel/TerminalViewModel.cs b/MultiTerm.Core/ViewModel/TerminalViewModel.cs index 9af35e4..9af6d0b 100644 --- a/MultiTerm.Core/ViewModel/TerminalViewModel.cs +++ b/MultiTerm.Core/ViewModel/TerminalViewModel.cs @@ -1,6 +1,6 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using MultiTerm.Core.Common; +using MultiTerm.Core.Types; namespace MultiTerm.Core.ViewModel; diff --git a/MultiTerm.Wpf/App.xaml.cs b/MultiTerm.Wpf/App.xaml.cs index dcb58aa..03428b4 100644 --- a/MultiTerm.Wpf/App.xaml.cs +++ b/MultiTerm.Wpf/App.xaml.cs @@ -4,9 +4,9 @@ using MultiTerm.Core.ViewModel; using System.Windows; using MultiTerm.Core.Helpers; using Common.Logging; -using Common.Logger; using System; using System.Threading.Tasks; +using Common.AppSettings; namespace MultiTerm.Wpf; @@ -24,6 +24,7 @@ public partial class App : Application { services.AddSingleton(); services.AddSingleton(new SerilogLogger("C:/log/multiterm-log-.txt", true)); + services.AddSingleton(new XmlAppSettingsProvider("C:/log/multiterm-config.xml")); // viewmodels services.AddSingleton(); @@ -48,6 +49,10 @@ public partial class App : Application Application.Current.DispatcherUnhandledException += this.Application_DispatcherUnhandledException; TaskScheduler.UnobservedTaskException += this.TaskScheduler_UnobservedTaskException; + // register log events from AppSettingsProvider + var appSettingsProvider = AppHost.Services.GetRequiredService(); + appSettingsProvider.LogWorthyEvent += AppSettingsProvider_LogWorthyEvent; + // instanciate startup form and show var startupForm = AppHost.Services.GetRequiredService(); startupForm.Show(); @@ -55,6 +60,11 @@ public partial class App : Application base.OnStartup(e); } + private void AppSettingsProvider_LogWorthyEvent(object? sender, LogEntry e) + { + logger?.Log(e); + } + protected override async void OnExit(ExitEventArgs e) { // log application exit and stop logger (if still available) diff --git a/MultiTerm.Wpf/MultiTerm.Wpf.csproj b/MultiTerm.Wpf/MultiTerm.Wpf.csproj index 458f24a..dd866f9 100644 --- a/MultiTerm.Wpf/MultiTerm.Wpf.csproj +++ b/MultiTerm.Wpf/MultiTerm.Wpf.csproj @@ -18,4 +18,8 @@ + + + + diff --git a/MultiTerm.Wpf/View/ShellView.xaml b/MultiTerm.Wpf/View/ShellView.xaml index 76a4e10..6518bb2 100644 --- a/MultiTerm.Wpf/View/ShellView.xaml +++ b/MultiTerm.Wpf/View/ShellView.xaml @@ -9,7 +9,7 @@ xmlns:custom_controls="clr-namespace:MultiTerm.Wpf.CustomControl;assembly=MultiTerm.Wpf.CustomControl" xmlns:vm="clr-namespace:MultiTerm.Core.ViewModel;assembly=MultiTerm.Core" xmlns:v="clr-namespace:MultiTerm.Wpf.View" - xmlns:core_common="clr-namespace:MultiTerm.Core.Common;assembly=MultiTerm.Core" + xmlns:types="clr-namespace:MultiTerm.Core.Types;assembly=MultiTerm.Core" xmlns:helpers="clr-namespace:MultiTerm.Wpf.Helpers" mc:Ignorable="d" d:DesignHeight="600" d:DesignWidth="1200"> @@ -22,21 +22,21 @@ ObjectType="{x:Type sys:Enum}" MethodName="GetValues"> - + - + - + @@ -111,7 +111,7 @@ + CommandParameterType="{x:Type types:ProtocolType}">