From 493260dac7c5df43c5979f6d0aa952f2eac0d3ad Mon Sep 17 00:00:00 2001 From: Jonas Arnold Date: Sun, 23 Apr 2023 20:21:28 +0200 Subject: [PATCH] implemented newline handling in CommunicationDataViewModel, implemented handling when newline separator is changed, removed newline sequence variables from IProtocolSettings, changed display style of RealizedItems in MultiFormatDataView, introduced Selector for NewlineSeparator in MultiFormatDataView --- .../ViewModel/CommunicationDataViewModel.cs | 211 +++++++++++++++++- .../ViewModel/SendReceiveViewModel.cs | 9 +- MultiTerm.Core/ViewModel/TerminalViewModel.cs | 99 +++++++- MultiTerm.Protocols/IProtocolSettings.cs | 10 - .../Serial/SerialProtocolSettingsViewModel.cs | 6 - .../MultiFormatDataView.cs | 73 +++++- .../MultiFormatDataView.xaml | 94 +++++--- MultiTerm.Wpf/View/SendReceiveView.xaml | 25 ++- 8 files changed, 460 insertions(+), 67 deletions(-) diff --git a/MultiTerm.Core/ViewModel/CommunicationDataViewModel.cs b/MultiTerm.Core/ViewModel/CommunicationDataViewModel.cs index db28987..ca82ded 100644 --- a/MultiTerm.Core/ViewModel/CommunicationDataViewModel.cs +++ b/MultiTerm.Core/ViewModel/CommunicationDataViewModel.cs @@ -1,5 +1,6 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using MultiTerm.Core.Types; using MultiTerm.Protocols; using MultiTerm.Protocols.Model; using System.Collections.ObjectModel; @@ -9,6 +10,8 @@ namespace MultiTerm.Core.ViewModel; public partial class CommunicationDataViewModel : ObservableObject { private ICommunicationProtocol? communicationProtocol; + private NewlineSeparatorType currentReceiveNewlineSeparatorType = NewlineSeparatorType.None; + private NewlineSeparatorType currentSentNewlineSeparatorType = NewlineSeparatorType.None; /// /// Represents the collection of received characters from a communication protocol. @@ -76,22 +79,220 @@ public partial class CommunicationDataViewModel : ObservableObject } } - private void CommunicationProtocol_ReceivedDataEvent(object? sender, ReceivedDataEventArgs e) + /// + /// Overwrites local variables for current newline separators and triggers recration of datacollections according to new newline separators (only if they changed). + /// + /// new receive newline separator type or null + /// new send newline separator type or null + public void ConfigureNewlineSeparators(NewlineSeparatorType? receiveNewlineSeparatorType, NewlineSeparatorType? sendNewlineSeparatorType) + { + if (receiveNewlineSeparatorType != null) + { + // if it is the same as the current newline separator type => ignore + if(receiveNewlineSeparatorType == this.currentReceiveNewlineSeparatorType) { return; } + + // set new newline separator type and overwrite data collection (triggers property changed) + this.currentReceiveNewlineSeparatorType = (NewlineSeparatorType)receiveNewlineSeparatorType; + this.ReceivedData = ReorderDataCollection(this.ReceivedData, this.currentReceiveNewlineSeparatorType); + } + if (sendNewlineSeparatorType != null) + { + // if it is the same as the current newline separator type => ignore + if (sendNewlineSeparatorType == this.currentSentNewlineSeparatorType) { return; } + + // set new newline separator type and overwrite data collection (triggers property changed) + this.currentSentNewlineSeparatorType = (NewlineSeparatorType)sendNewlineSeparatorType; + this.SentData = ReorderDataCollection(this.SentData, this.currentSentNewlineSeparatorType); + } + } + + /// + /// Function that reorders a given collection using the given . + /// Reordered collection is returned, but LineIdentifier is also overwritten in the parameter. + /// + /// items in this collection will be reordered + /// separator between lines + /// reordered collection + private static ObservableCollection ReorderDataCollection(ObservableCollection currentCollection, NewlineSeparatorType newlineSeparatorType) + { + ObservableCollection newCollection = new(); + int lineCounter = 0; + List? previousCharacters = null; + + foreach (var item in currentCollection) + { + item.LineIdentifier = lineCounter; + newCollection.Add(item); + switch (ShouldIntroduceNewlineAfterThisCharacter(item.Character.Character, previousCharacters, newlineSeparatorType)) + { + case IntroduceNewlineAfterThisCharacterResult.NoNewline: + // a decision could be made, previousCharacters can be cleared + if(previousCharacters != null) { previousCharacters = null; } + // nothing to do + break; + + case IntroduceNewlineAfterThisCharacterResult.IntroduceNewline: + // a decision could be made, previousCharacters can be cleared + if (previousCharacters != null) { previousCharacters = null; } + // increase line count and break + lineCounter++; + break; + + case IntroduceNewlineAfterThisCharacterResult.RequiresMoreCharacters: + // first time that more characters are required => create new list + previousCharacters ??= new List(); + // add current character to list + previousCharacters.Add(item.Character.Character); + break; + + default: + throw new Exception($"'{nameof(ReorderDataCollection)}()' failed because of error when checking if a newline should be introduced."); + } + } + + return newCollection; + } + + /// + /// Result type for + /// + public enum IntroduceNewlineAfterThisCharacterResult + { + /// + /// No newline is required. + /// + NoNewline, + + /// + /// A newline shall be introduced after this character. + /// + IntroduceNewline, + + /// + /// Following characters are required to finalize result wether a newline shall be introduced or not. + /// + RequiresMoreCharacters + } + + /// + /// Function to check wether a newline shall be introduced after the given character. + /// Since some newline sequences will require multiple characters in correct order, a more complex handling is required, which is possible using this function. + /// + /// the current character in the collection (or a single character) + /// list of previous character, newest at the last position of the list, null if not required + /// separator type + /// complex result of type + /// if the handling for the is not implemented + private static IntroduceNewlineAfterThisCharacterResult ShouldIntroduceNewlineAfterThisCharacter(char character, List? previousCharacters, NewlineSeparatorType newlineSeparatorType) { - foreach (var receivedChar in e.ReceivedCharacters) + var result = IntroduceNewlineAfterThisCharacterResult.NoNewline; + + switch (newlineSeparatorType) { - //this.ReceivedData.Add(receivedChar); // TODO Fix + case NewlineSeparatorType.None: + break; + + case NewlineSeparatorType.CR: + if(character == '\r') + { + result = IntroduceNewlineAfterThisCharacterResult.IntroduceNewline; + } + break; + + case NewlineSeparatorType.LF: + if (character == '\n') + { + result = IntroduceNewlineAfterThisCharacterResult.IntroduceNewline; + } + break; + + case NewlineSeparatorType.CR_LF: + if (character == '\r') + { + result = IntroduceNewlineAfterThisCharacterResult.RequiresMoreCharacters; + } + if (character == '\n') + { + if(previousCharacters != null && previousCharacters.Last() == '\r') + { + result = IntroduceNewlineAfterThisCharacterResult.IntroduceNewline; + } + } + break; + + default: + throw new NotImplementedException($"'{nameof(ShouldIntroduceNewlineAfterThisCharacter)}()' does not implement handling for {nameof(NewlineSeparatorType)} {newlineSeparatorType}"); } + + return result; } + private int receivedDataCharacterCount = 0; + private List? listOfPreviouslyReceivedCharacters = null; + private void CommunicationProtocol_ReceivedDataEvent(object? sender, ReceivedDataEventArgs e) + { + HandleNewCharacters(this.ReceivedData, ref this.receivedDataCharacterCount, ref this.listOfPreviouslyReceivedCharacters, this.currentReceiveNewlineSeparatorType, e.ReceivedCharacters); + } + + private int sentDataCharacterCount = 0; + private List? listOfPreviouslySentCharacters = null; private void CommunicationProtocol_SentDataEvent(object? sender, SentDataEventArgs e) { - foreach (var sentChar in e.SentCharacters) + HandleNewCharacters(this.SentData, ref this.sentDataCharacterCount, ref this.listOfPreviouslySentCharacters, this.currentSentNewlineSeparatorType, e.SentCharacters); + } + + /// + /// Function that handles a list of new characters that should end up in the collection . + /// In case a new line is required, according to the given , it is automatically introduced. + /// Following parameters need to be referenced and stored outside: and . + /// + /// collection to add the characters to + /// current line count + /// list of previous character, newest at the last position of the list, null if nothing is stored + /// separator between seperate lines + /// characters to add to the + /// in case of any error + private void HandleNewCharacters(ObservableCollection dataCollection, + ref int collectionLineCounter, + ref List? previousCharacters, + NewlineSeparatorType newlineSeparatorType, + IEnumerable characters) + { + // go through every character + foreach (var newExtdChar in characters) { - //this.SentCharacters.Add(sentChar); // TODO Fix + // add to collection with the current counter + dataCollection.Add(new DataViewModel(newExtdChar, collectionLineCounter)); + + switch (ShouldIntroduceNewlineAfterThisCharacter(newExtdChar.Character, previousCharacters, newlineSeparatorType)) + { + case IntroduceNewlineAfterThisCharacterResult.NoNewline: + // a decision could be made, previousCharacters can be cleared + if (previousCharacters != null) { previousCharacters = null; } + // nothing to do + break; + + case IntroduceNewlineAfterThisCharacterResult.IntroduceNewline: + // a decision could be made, previousCharacters can be cleared + if (previousCharacters != null) { previousCharacters = null; } + // increase line count and break + collectionLineCounter++; + break; + + case IntroduceNewlineAfterThisCharacterResult.RequiresMoreCharacters: + // first time that more characters are required => create new list + previousCharacters ??= new List(); + // add current character to list + previousCharacters.Add(newExtdChar.Character); + break; + + default: + throw new Exception($"'{nameof(HandleNewCharacters)}()' failed because of error when checking if a newline should be introduced."); + } } } + [RelayCommand] private void ClearReceivedData() { diff --git a/MultiTerm.Core/ViewModel/SendReceiveViewModel.cs b/MultiTerm.Core/ViewModel/SendReceiveViewModel.cs index 2f95769..f0d99dd 100644 --- a/MultiTerm.Core/ViewModel/SendReceiveViewModel.cs +++ b/MultiTerm.Core/ViewModel/SendReceiveViewModel.cs @@ -1,4 +1,5 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using Common.AppSettings; +using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using MultiTerm.Core.Model; using MultiTerm.Core.Types; @@ -30,6 +31,12 @@ public partial class SendReceiveViewModel : TerminalViewModel [ObservableProperty] private string sentData = string.Empty; + /// + /// Constructor. + /// + /// + public SendReceiveViewModel(IAppSettingsProvider appSettings) : base(appSettings) { } + /// /// Send command. /// diff --git a/MultiTerm.Core/ViewModel/TerminalViewModel.cs b/MultiTerm.Core/ViewModel/TerminalViewModel.cs index 71f7bc5..1ef9dd8 100644 --- a/MultiTerm.Core/ViewModel/TerminalViewModel.cs +++ b/MultiTerm.Core/ViewModel/TerminalViewModel.cs @@ -1,4 +1,6 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using Common.AppSettings; +using Common.Helpers; +using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using MultiTerm.Core.Types; using MultiTerm.Protocols; @@ -8,15 +10,32 @@ namespace MultiTerm.Core.ViewModel; public abstract partial class TerminalViewModel : ObservableObject, ITerminalViewModel { + private const string defaultReceiveNewlineSeparatorAppSettingsKey = "DefaultReceiveNewlineSeparator"; + private const string defaultSendNewlineSeparatorAppSettingsKey = "DefaultSendNewlineSeparator"; + private readonly IAppSettingsProvider appSettings; + public abstract string Title { get; } public abstract TerminalViewType ViewType { get; } public ProtocolType ProtocolType { get; set; } - public event EventHandler? ClosingEvent; - + /// + /// Holds communication data, meaning data that was sent to or received over the communication protocol. + /// [ObservableProperty] private CommunicationDataViewModel communicationData = new(null); + /// + /// Newline Separator Type that is selected for receival data of this Terminal. Defaults to none. + /// + [ObservableProperty] + private NewlineSeparatorType receiveNewlineSeparatorType = NewlineSeparatorType.None; + + /// + /// Newline Separator Type that is selected for sent out data of this Terminal. Defaults to none. + /// + [ObservableProperty] + private NewlineSeparatorType sendNewlineSeparatorType = NewlineSeparatorType.None; + private ICommunicationProtocol? communicationProtocol; public ICommunicationProtocol? CommunicationProtocol { @@ -26,6 +45,9 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie communicationProtocol = value; // register communication protocol in the Communication Data View Model //this.CommunicationData = new CommunicationDataViewModel(communicationProtocol); // TEMP + + // initialize both newline Separators. initializes with null if they are not available. + this.CommunicationData.ConfigureNewlineSeparators(this.ReceiveNewlineSeparatorType, this.SendNewlineSeparatorType); } } @@ -39,12 +61,39 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie // register event handler for connection request from viewmodel if(value != null) { - protocolSettings!.ConnectRequested += OnViewModelRequestedConnect; ; ; + protocolSettings!.ConnectRequested += OnViewModelRequestedConnect; protocolSettings!.DisconnectRequested += OnViewModelRequestedDisconnect; } } } + public TerminalViewModel(IAppSettingsProvider appSettings) + { + this.appSettings = appSettings; + this.InitializeNewlineSeparatorsFromAppSettings(); + } + + #region Connect/Disconnect + private void OnViewModelRequestedConnect(object? sender, ConnectionRequestEventArgs e) + { + // guard uninitialized CommunicationProtocol + if (CommunicationProtocol == null) { throw new Exception($"To call '{nameof(OnViewModelRequestedConnect)}()', CommunicationProtocol must not be null!"); } + + e.Success = this.CommunicationProtocol.Connect(e.Settings); + } + + private void OnViewModelRequestedDisconnect(object? sender, EventArgs e) + { + // guard uninitialized CommunicationProtocol + if (CommunicationProtocol == null) { throw new Exception($"To call '{nameof(OnViewModelRequestedConnect)}()', CommunicationProtocol must not be null!"); } + + this.CommunicationProtocol.Disconnect(); + } + #endregion + + #region Closing handling + public event EventHandler? ClosingEvent; + /// /// Method to override if any closing actions are required. /// Closing can be cancelled using the return value. @@ -60,20 +109,46 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie ClosingEvent?.Invoke(this, EventArgs.Empty); } } + #endregion - private void OnViewModelRequestedConnect(object? sender, ConnectionRequestEventArgs e) + #region Newline Separator handling + /// + /// Gets the default settings for both newline separators from the AppSettings Provider. + /// Writes them to the local variables and . + /// In case there are no default settings, it does not overwrite the local variables. + /// In case the Communication Data is null, nothing is done. + /// + private void InitializeNewlineSeparatorsFromAppSettings() { - // guard uninitialized CommunicationProtocol - if (CommunicationProtocol == null) { throw new Exception($"To call '{nameof(OnViewModelRequestedConnect)}()', CommunicationProtocol must not be null!"); } + // guard null objects + if(this.CommunicationData == null) { return; } - e.Success = this.CommunicationProtocol.Connect(e.Settings); + // get newline separators from persistent settings, if not available catch exceptions + this.appSettings.TryReadSetting(defaultReceiveNewlineSeparatorAppSettingsKey, out string settingValueReceiveNLSep); + try + { + this.ReceiveNewlineSeparatorType = EnumHelpers.ParseEnum(settingValueReceiveNLSep); + } + catch { } + this.appSettings.TryReadSetting(defaultSendNewlineSeparatorAppSettingsKey, out string settingValueSendNLSep); + try + { + this.SendNewlineSeparatorType = EnumHelpers.ParseEnum(settingValueSendNLSep); + } + catch { } } - private void OnViewModelRequestedDisconnect(object? sender, EventArgs e) + partial void OnReceiveNewlineSeparatorTypeChanged(NewlineSeparatorType value) { - // guard uninitialized CommunicationProtocol - if (CommunicationProtocol == null) { throw new Exception($"To call '{nameof(OnViewModelRequestedConnect)}()', CommunicationProtocol must not be null!"); } + // triggers rearranging the received data with the new NewlineSeparatorType + this.CommunicationData.ConfigureNewlineSeparators(value, null); + } - this.CommunicationProtocol.Disconnect(); + partial void OnSendNewlineSeparatorTypeChanged(NewlineSeparatorType value) + { + // triggers rearranging the sent data with the new NewlineSeparatorType + this.CommunicationData.ConfigureNewlineSeparators(null, value); } + #endregion + } diff --git a/MultiTerm.Protocols/IProtocolSettings.cs b/MultiTerm.Protocols/IProtocolSettings.cs index d77569d..6fbeaa3 100644 --- a/MultiTerm.Protocols/IProtocolSettings.cs +++ b/MultiTerm.Protocols/IProtocolSettings.cs @@ -2,16 +2,6 @@ public interface IProtocolSettings { - /// - /// Newline sequence to add on the end when sending a message. - /// - string NewlineSequenceOnSend { get; } - - /// - /// When this sequence is detected while reading, a new line is introduced. - /// - string NewlineOnReceivedSequence { get; } - /// /// Checks if the entered settings are valid and a connection can be made. /// diff --git a/MultiTerm.Protocols/Serial/SerialProtocolSettingsViewModel.cs b/MultiTerm.Protocols/Serial/SerialProtocolSettingsViewModel.cs index 138ae4e..e79b671 100644 --- a/MultiTerm.Protocols/Serial/SerialProtocolSettingsViewModel.cs +++ b/MultiTerm.Protocols/Serial/SerialProtocolSettingsViewModel.cs @@ -37,12 +37,6 @@ public partial class SerialProtocolSettingsViewModel : ProtocolSettingsViewModel this.ComPorts = SerialProtocol.GetPortNames(); } - // TODO - public string NewlineSequenceOnSend => throw new NotImplementedException(); - - public string NewlineOnReceivedSequence => throw new NotImplementedException(); - - public SerialProtocolSettingsViewModel(IMessenger messenger) : base(messenger) { // load com ports initially diff --git a/MultiTerm.Wpf.CustomControl/MultiFormatDataView/MultiFormatDataView.cs b/MultiTerm.Wpf.CustomControl/MultiFormatDataView/MultiFormatDataView.cs index 67bf6ce..ed36590 100644 --- a/MultiTerm.Wpf.CustomControl/MultiFormatDataView/MultiFormatDataView.cs +++ b/MultiTerm.Wpf.CustomControl/MultiFormatDataView/MultiFormatDataView.cs @@ -14,8 +14,8 @@ public class MultiFormatDataView : Control private static readonly Dictionary itemParentPairs = new(); private const string itemsControlTemplateName = "itemsControl"; private const string buttonClearTemplateName = "btnClear"; + private const string selectorTemplateName = "comboBoxSelector"; private ListBox? itemsControl; - private List currentSelectedItems = new(); #region Dependency Properties public static readonly DependencyProperty DataSourceProperty = @@ -31,6 +31,21 @@ public class MultiFormatDataView : Control BindsTwoWayByDefault = false }); + public static readonly DependencyProperty SelectorItemsSourceProperty = + DependencyProperty.Register("SelectorItemsSource", + typeof(IEnumerable), typeof(MultiFormatDataView), + new PropertyMetadata(null, OnSelectorItemsSourceChanged)); + + public static readonly DependencyProperty SelectorSelectedItemProperty = + DependencyProperty.Register("SelectorSelectedItem", + typeof(object), typeof(MultiFormatDataView), + new PropertyMetadata(null, OnSelectorSelectedItemChanged)); + + public static readonly DependencyProperty SelectorDescriptionProperty = + DependencyProperty.Register("SelectorDescription", + typeof(string), typeof(MultiFormatDataView), + new PropertyMetadata(string.Empty, OnSelectorDescriptionChanged)); + public static readonly DependencyProperty RealizedItemsCountProperty = DependencyProperty.Register("RealizedItemsCount", typeof(uint), typeof(MultiFormatDataView), @@ -70,6 +85,36 @@ public class MultiFormatDataView : Control set { SetValue(SelectedItemsProperty, value); } } + /// + /// .NET Property for SelectorItemsSource. + /// + [Bindable(true)] + public IEnumerable SelectorItemsSource + { + get { return (IEnumerable)GetValue(SelectorItemsSourceProperty); } + set { SetValue(SelectorItemsSourceProperty, value); } + } + + /// + /// .NET Property for SelectorSelectedItem. + /// + [Bindable(true)] + public string SelectorSelectedItem + { + get { return (string)GetValue(SelectorSelectedItemProperty); } + set { SetValue(SelectorSelectedItemProperty, value); } + } + + /// + /// .NET Property for SelectorDescription. + /// + [Bindable(true)] + public string SelectorDescription + { + get { return (string)GetValue(SelectorDescriptionProperty); } + set { SetValue(SelectorDescriptionProperty, value); } + } + /// /// .NET Property for RealizedItemsCount. /// @@ -169,9 +214,6 @@ public class MultiFormatDataView : Control #region Selected Items handling private void ItemsControl_SelectionChanged(object sender, SelectionChangedEventArgs e) { - if(this.currentSelectedItems == null) - { this.currentSelectedItems = new List(); } - // if there are no changes => return if (e.AddedItems.Count <= 0 && e.RemovedItems.Count <= 0) { return; } @@ -192,6 +234,29 @@ public class MultiFormatDataView : Control } #endregion + #region Selector (ComboBox) handling + private static void OnSelectorItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + // extract instance and guard null + //if (d is not MultiFormatDataView mfdv) { return; } + // extract instance of new Value and guard null + //if (e.NewValue is not IEnumerable enumerable) { return; } + + // nothing to do + } + + private static void OnSelectorDescriptionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + // nothing to do + } + + private static void OnSelectorSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + // nothing to do + } + + #endregion + #region Realized Item Count private static void OnRealizedItemsCountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { diff --git a/MultiTerm.Wpf.CustomControl/MultiFormatDataView/MultiFormatDataView.xaml b/MultiTerm.Wpf.CustomControl/MultiFormatDataView/MultiFormatDataView.xaml index e019d7f..f41b3e3 100644 --- a/MultiTerm.Wpf.CustomControl/MultiFormatDataView/MultiFormatDataView.xaml +++ b/MultiTerm.Wpf.CustomControl/MultiFormatDataView/MultiFormatDataView.xaml @@ -115,42 +115,82 @@ - + + + -