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 @@ - + + + -