finished implementation of XmlAppSettingsProvider,

added description to IAppSettingsProvider methods,
added loading and saving to settings on app startup and exit,
cleaned up ShellViewModel
master
Jonas Arnold 3 years ago
parent 5312b750f5
commit 95a9eec8e4
  1. 28
      Common/AppSettings/IAppSettingsProvider.cs
  2. 42
      Common/AppSettings/XmlAppSettingsProvider.cs
  3. 37
      MultiTerm.Core/ViewModel/ShellViewModel.cs
  4. 7
      MultiTerm.Wpf/App.xaml.cs

@ -12,18 +12,44 @@ public interface IAppSettingsProvider
/// <summary> /// <summary>
/// Retrieves the app settings from the persistant location and loads them into memory. /// Retrieves the app settings from the persistant location and loads them into memory.
/// Loading settings will override the currently stored settings in the memory. /// Loading settings will override the currently stored settings in the memory.
/// If loading happens and there were already settings in the memory, a <see cref="LogWorthyEvent"/> is generated.
/// </summary> /// </summary>
void Load(); void Load();
/// <summary> /// <summary>
/// Saves the app settings to the persistant location. /// Saves the app settings to the persistant location.
/// If the saving does not work, logs the exception as a <see cref="LogWorthyEvent"/> but does not throw an exception.
/// Rationale: A User is happier if the app is useable and does not remember settings than if the app always throws an exception on startup.
/// </summary> /// </summary>
void Save(); void Save();
/// <summary>
/// Creates a new setting or overwrites the value of an existing setting in the memory (not persistent location). Settings are identified by key.
/// Informs about issues using the <see cref="LogWorthyEvent"/>.
/// </summary>
/// <param name="key">key of the setting, must not be empty or null</param>
/// <param name="value">value of the setting, must not be empty or null</param>
/// <returns>true if the setting is successfully written or overwritten</returns>
bool WriteSetting(string key, string value); bool WriteSetting(string key, string value);
/// <summary>
/// Tries to read a setting from the memory (not persistent location). Settings are identified by key.
/// Informs about issues using the <see cref="LogWorthyEvent"/>.
/// If no setting with the given <paramref name="key"/> is found, the <paramref name="value"/> is set to <see cref="String.Empty"/>.
/// </summary>
/// <param name="key">key of the setting, must not be empty or null</param>
/// <param name="value">the found value or an empty string if no setting is found</param>
/// <returns>true when the setting exists and could be read successfully</returns>
bool TryReadSetting(string key, out string value); bool TryReadSetting(string key, out string value);
/// <summary>
/// Tries to read a setting from the memory (not persistent location). Settings are identified by key.
/// Informs about issues using the <see cref="LogWorthyEvent"/>.
/// If no setting with the given <paramref name="key"/> is found, the setting is created with the given <paramref name="defaultValue"/>. <paramref name="value"/> is also set to <paramref name="defaultValue"/>.
/// </summary>
/// <param name="key">key of the setting, must not be empty or null</param>
/// <param name="value">the found value or an empty string if no setting is found</param>
/// <param name="defaultValue">the default value to initialize the setting with if it does not exist yet. must not be empty or null</param>
/// <returns>true when the setting could be read or was created successfully</returns>
bool TryReadSettingOrAddDefault(string key, out string value, string defaultValue); bool TryReadSettingOrAddDefault(string key, out string value, string defaultValue);
} }

@ -14,12 +14,17 @@ public class XmlAppSettingsProvider : IAppSettingsProvider
public XmlAppSettingsProvider(string settingsFilePath) public XmlAppSettingsProvider(string settingsFilePath)
{ {
// check for file path validity // check for file path validity
// TODO create file if it does not exist yet
if (String.IsNullOrEmpty(settingsFilePath)) { throw new ArgumentNullException(nameof(settingsFilePath)); } if (String.IsNullOrEmpty(settingsFilePath)) { throw new ArgumentNullException(nameof(settingsFilePath)); }
this.settingsFilePath = settingsFilePath; this.settingsFilePath = settingsFilePath;
this.settings = new List<AppSetting>(); this.settings = new List<AppSetting>();
this.serializer = new XmlSerializer(typeof(List<AppSetting>)); this.serializer = new XmlSerializer(typeof(List<AppSetting>));
// create file if it does not exist yet
if (File.Exists(settingsFilePath) == false)
{
this.Save();
}
} }
public void Load() public void Load()
@ -35,24 +40,33 @@ public class XmlAppSettingsProvider : IAppSettingsProvider
if(this.settings.Count > 0) if(this.settings.Count > 0)
{ {
this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Warn, category: nameof(XmlAppSettingsProvider), this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Warn, category: nameof(XmlAppSettingsProvider),
message: $"'{nameof(Load)}'() deserialization overwrote previously loaded settings.")); message: $"'{nameof(Load)}()' deserialization overwrote previously loaded settings."));
} }
this.settings = (List<AppSetting>)deserializedObj; this.settings = (List<AppSetting>)deserializedObj;
} }
else else
{ {
this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Warn, category: nameof(XmlAppSettingsProvider), 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.")); message: $"'{nameof(Load)}()' deserialization resulted in null object. Did not change loaded settings."));
} }
} }
public void Save() public void Save()
{ {
// TODO write log entries for when something goes wrong // if saving fails, just log the exception. user is happier if the app is useable but does not remember settings
try
{
// create file and serialize data
TextWriter writer = new StreamWriter(this.settingsFilePath); TextWriter writer = new StreamWriter(this.settingsFilePath);
this.serializer.Serialize(writer, this.settings); this.serializer.Serialize(writer, this.settings);
writer.Close(); writer.Close();
} }
catch (Exception ex)
{
this.LogWorthyEvent?.Invoke(this, new LogEntry(){ LogLevel = LogLevel.Error, Category = nameof(XmlAppSettingsProvider),
Message = $"'{nameof(Save)}()' failed to create file or serialize data.", Exception = ex });
}
}
@ -69,7 +83,7 @@ public class XmlAppSettingsProvider : IAppSettingsProvider
if(foundSettings.Count > 1) if(foundSettings.Count > 1)
{ {
this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Error, category: nameof(XmlAppSettingsProvider), 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.")); message: $"'{nameof(WriteSetting)}()' cancelled because {foundSettings.Count} settings with the key '{key}' were found. Expected none or exactly one."));
return false; // cancel return false; // cancel
} }
@ -82,7 +96,7 @@ public class XmlAppSettingsProvider : IAppSettingsProvider
else if(foundSettings.Count == 0) else if(foundSettings.Count == 0)
{ {
this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Info, category: nameof(XmlAppSettingsProvider), this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Info, category: nameof(XmlAppSettingsProvider),
message: $"'{nameof(WriteSetting)}'() initialized new setting with key '{key}' and value '{value}'.")); message: $"'{nameof(WriteSetting)}()' initialized new setting with key '{key}' and value '{value}'."));
} }
// add new setting // add new setting
@ -102,7 +116,7 @@ public class XmlAppSettingsProvider : IAppSettingsProvider
if (foundSettings.Count > 1) if (foundSettings.Count > 1)
{ {
this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Error, category: nameof(XmlAppSettingsProvider), 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.")); message: $"'{nameof(TryReadSetting)}()' did find {foundSettings.Count} settings with the key '{key}'. Expected only one."));
value = string.Empty; value = string.Empty;
return false; return false;
@ -112,7 +126,7 @@ public class XmlAppSettingsProvider : IAppSettingsProvider
if (foundSettings.Count == 0) if (foundSettings.Count == 0)
{ {
this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Error, category: nameof(XmlAppSettingsProvider), this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Error, category: nameof(XmlAppSettingsProvider),
message: $"'{nameof(TryReadSetting)}'() did not find a setting with the key '{key}'.")); message: $"'{nameof(TryReadSetting)}()' did not find a setting with the key '{key}'."));
value = string.Empty; value = string.Empty;
return false; return false;
@ -128,11 +142,10 @@ public class XmlAppSettingsProvider : IAppSettingsProvider
else else
{ {
this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Warn, category: nameof(XmlAppSettingsProvider), 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.")); message: $"'{nameof(TryReadSetting)}()' value of retrieved setting with key '{key}' was null. Returned empty string instead."));
value = string.Empty; value = string.Empty;
} }
return true; return true;
} }
@ -149,7 +162,7 @@ public class XmlAppSettingsProvider : IAppSettingsProvider
if (foundSettings.Count > 1) if (foundSettings.Count > 1)
{ {
this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Error, category: nameof(XmlAppSettingsProvider), 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.")); message: $"'{nameof(TryReadSettingOrAddDefault)}()' did find {foundSettings.Count} settings with the key '{key}'. Expected only one."));
value = string.Empty; value = string.Empty;
return false; return false;
@ -159,11 +172,10 @@ public class XmlAppSettingsProvider : IAppSettingsProvider
if (foundSettings.Count == 0) if (foundSettings.Count == 0)
{ {
this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Info, category: nameof(XmlAppSettingsProvider), 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}'.")); message: $"'{nameof(TryReadSettingOrAddDefault)}()' did not find a setting with the key '{key}'. Adding setting with value '{defaultValue}'."));
this.WriteSetting(key, defaultValue);
value = defaultValue; value = defaultValue;
return true; return this.WriteSetting(key, defaultValue);
} }
// extract correct setting when found exactly one // extract correct setting when found exactly one
@ -176,7 +188,7 @@ public class XmlAppSettingsProvider : IAppSettingsProvider
else else
{ {
this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Warn, category: nameof(XmlAppSettingsProvider), 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.")); message: $"'{nameof(TryReadSetting)}()' value of retrieved setting with key '{key}' was null. Returned empty string instead."));
value = string.Empty; value = string.Empty;
} }

@ -14,21 +14,18 @@ public partial class ShellViewModel : ObservableObject
private const string defaultReceiveNewlineSeparatorAppSettingsKey = "DefaultReceiveNewlineSeparator"; private const string defaultReceiveNewlineSeparatorAppSettingsKey = "DefaultReceiveNewlineSeparator";
private const string defaultSendNewlineSeparatorAppSettingsKey = "DefaultSendNewlineSeparator"; private const string defaultSendNewlineSeparatorAppSettingsKey = "DefaultSendNewlineSeparator";
[ObservableProperty] #region Terminal collection
private string title = "ShellView Test";
[ObservableProperty] [ObservableProperty]
private ObservableCollection<ITerminalViewModel> terminalViewModels = new(); private ObservableCollection<ITerminalViewModel> terminalViewModels = new();
[ObservableProperty] [ObservableProperty]
private ITerminalViewModel? selectedTerminalViewModel; private ITerminalViewModel? selectedTerminalViewModel;
#endregion
#region Settings Menu Bar #region Settings Menu Bar
// TODO Initialize from File
[ObservableProperty] [ObservableProperty]
private NewlineSeparatorType defaultReceiveNewlineSeparator = NewlineSeparatorType.None; private NewlineSeparatorType defaultReceiveNewlineSeparator = NewlineSeparatorType.None;
// TODO Initialize from File
[ObservableProperty] [ObservableProperty]
private NewlineSeparatorType defaultSendNewlineSeparator = NewlineSeparatorType.None; private NewlineSeparatorType defaultSendNewlineSeparator = NewlineSeparatorType.None;
#endregion #endregion
@ -36,7 +33,6 @@ public partial class ShellViewModel : ObservableObject
#region New Terminal Context Menu #region New Terminal Context Menu
[ObservableProperty] [ObservableProperty]
private TerminalViewType selectedTerminalViewType = TerminalViewType.SendReceive; private TerminalViewType selectedTerminalViewType = TerminalViewType.SendReceive;
#endregion #endregion
private readonly ITerminalViewModelFactory terminalViewModelFactory; private readonly ITerminalViewModelFactory terminalViewModelFactory;
@ -49,11 +45,8 @@ public partial class ShellViewModel : ObservableObject
this.logger = logger; this.logger = logger;
this.appSettings = appSettings; this.appSettings = appSettings;
// initialize newline separators from persistent settings // intialize values from app settings
this.appSettings.TryReadSettingOrAddDefault(defaultReceiveNewlineSeparatorAppSettingsKey, out string settingValueReceiveNLSep, NewlineSeparatorType.None.ToString()); this.LoadFromAppSettings();
this.DefaultReceiveNewlineSeparator = EnumHelpers.ParseEnum<NewlineSeparatorType>(settingValueReceiveNLSep);
this.appSettings.TryReadSettingOrAddDefault(defaultSendNewlineSeparatorAppSettingsKey, out string settingValueSendNLSep, NewlineSeparatorType.None.ToString());
this.DefaultSendNewlineSeparator = EnumHelpers.ParseEnum<NewlineSeparatorType>(settingValueSendNLSep);
// TEMP Init // TEMP Init
this.AppendTerminalWithSelectedViewType(ProtocolType.Serial); this.AppendTerminalWithSelectedViewType(ProtocolType.Serial);
@ -94,20 +87,26 @@ public partial class ShellViewModel : ObservableObject
this.TerminalViewModels.Remove(terminalToRemove); this.TerminalViewModels.Remove(terminalToRemove);
} }
partial void OnDefaultReceiveNewlineSeparatorChanged(NewlineSeparatorType value) #region App Settings handling
private void LoadFromAppSettings()
{ {
Console.WriteLine($"Changed to {value}"); // initialize newline separators from persistent settings
this.appSettings.TryReadSettingOrAddDefault(defaultReceiveNewlineSeparatorAppSettingsKey, out string settingValueReceiveNLSep, NewlineSeparatorType.None.ToString());
this.DefaultReceiveNewlineSeparator = EnumHelpers.ParseEnum<NewlineSeparatorType>(settingValueReceiveNLSep);
this.appSettings.TryReadSettingOrAddDefault(defaultSendNewlineSeparatorAppSettingsKey, out string settingValueSendNLSep, NewlineSeparatorType.None.ToString());
this.DefaultSendNewlineSeparator = EnumHelpers.ParseEnum<NewlineSeparatorType>(settingValueSendNLSep);
} }
partial void OnDefaultSendNewlineSeparatorChanged(NewlineSeparatorType value) partial void OnDefaultReceiveNewlineSeparatorChanged(NewlineSeparatorType value)
{ {
Console.WriteLine($"Changed Send to {value}"); // update setting in app settings
this.appSettings.WriteSetting(defaultReceiveNewlineSeparatorAppSettingsKey, value.ToString());
} }
[RelayCommand] partial void OnDefaultSendNewlineSeparatorChanged(NewlineSeparatorType value)
private void TestButtonClicked()
{ {
this.DefaultReceiveNewlineSeparator = NewlineSeparatorType.CR_LF; // update setting in app settings
this.appSettings.WriteSetting(defaultSendNewlineSeparatorAppSettingsKey, value.ToString());
} }
#endregion
} }

@ -49,9 +49,10 @@ public partial class App : Application
Application.Current.DispatcherUnhandledException += this.Application_DispatcherUnhandledException; Application.Current.DispatcherUnhandledException += this.Application_DispatcherUnhandledException;
TaskScheduler.UnobservedTaskException += this.TaskScheduler_UnobservedTaskException; TaskScheduler.UnobservedTaskException += this.TaskScheduler_UnobservedTaskException;
// register log events from AppSettingsProvider // register log events from AppSettingsProvider and load saved settings
var appSettingsProvider = AppHost.Services.GetRequiredService<IAppSettingsProvider>(); var appSettingsProvider = AppHost.Services.GetRequiredService<IAppSettingsProvider>();
appSettingsProvider.LogWorthyEvent += AppSettingsProvider_LogWorthyEvent; appSettingsProvider.LogWorthyEvent += AppSettingsProvider_LogWorthyEvent;
appSettingsProvider.Load();
// instanciate startup form and show // instanciate startup form and show
var startupForm = AppHost.Services.GetRequiredService<MainWindow>(); var startupForm = AppHost.Services.GetRequiredService<MainWindow>();
@ -67,6 +68,10 @@ public partial class App : Application
protected override async void OnExit(ExitEventArgs e) protected override async void OnExit(ExitEventArgs e)
{ {
// save settings to persistent location
var appSettingsProvider = AppHost!.Services.GetRequiredService<IAppSettingsProvider>();
appSettingsProvider.Save();
// log application exit and stop logger (if still available) // log application exit and stop logger (if still available)
logger?.LogInfo("Application exited.", nameof(App)); logger?.LogInfo("Application exited.", nameof(App));
logger?.StopLogging(); logger?.StopLogging();

Loading…
Cancel
Save