using Common; using Common.AppSettings; using Common.Helpers; using Common.Messaging; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; using MultiTerm.Core.Types; using MultiTerm.Protocols; using MultiTerm.Protocols.Model; using MultiTerm.Protocols.Types; 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; private readonly IMessenger messenger; private readonly IContext context; public abstract TerminalViewType ViewType { get; } private ProtocolType protocolType; public ProtocolType ProtocolType { get { return this.protocolType; } set { this.protocolType = value; this.UpdateTitle(); } } #region Title /// /// Terminal title, to allow user to distinguish between different terminals. /// [ObservableProperty] private string title = string.Empty; /// /// Update the Property . To be called when it was changed. /// private void UpdateTitle() { if(this.CommunicationProtocol == null) { this.Title = $"new"; } else { this.Title = $"{this.CommunicationProtocol?.InstanceIdentifier}"; } } #endregion #region Observable Properties /// /// Holds communication data that was received via the communication protocol. /// [ObservableProperty] private ICommunicationDataViewModel receivedData; /// /// Holds communication data that was sent via the communication protocol. /// [ObservableProperty] private ICommunicationDataViewModel sentData; /// /// Defines at which newline sequence the displayed data is wrapped. Defaults to none. /// [ObservableProperty] private NewlineSeparatorType dataDisplayNewlineSeparatorType = NewlineSeparatorType.None; /// /// Defines which Newline sequence is attached to data when it is sent. Defaults to none. /// [ObservableProperty] private NewlineSeparatorType sendNewlineSeparatorType = NewlineSeparatorType.None; [ObservableProperty] private ICommunicationProtocol? communicationProtocol; [ObservableProperty] private IProtocolSettingsViewModel? protocolSettings; /// /// of as ObservableProperty. /// Required since does not implement . /// [ObservableProperty] private ProtocolConnectionState communicationProtocolConnectionState; #endregion public TerminalViewModel(IAppSettingsProvider appSettings, IMessenger messenger, IContext context) { this.appSettings = appSettings; this.messenger = messenger; this.context = context; // create new Communication data containers this.receivedData = new CommunicationDataViewModel(this.context); this.sentData = new CommunicationDataViewModel(this.context); } ~TerminalViewModel() { if (this.CommunicationProtocol != null) { // disconnect communication protocol this.OnViewModelRequestedDisconnect(this, EventArgs.Empty); // unsubscribe events this.CommunicationProtocol.ReceivedDataEvent -= CommunicationProtocol_ReceivedDataEvent; this.CommunicationProtocol.SentDataEvent -= CommunicationProtocol_SentDataEvent; this.CommunicationProtocol.ConnectionStateChangedEvent -= CommunicationProtocol_ConnectionStateChangedEvent; // remove reference to object this.CommunicationProtocol = null; } } #region Communication Protocol partial void OnCommunicationProtocolChanged(ICommunicationProtocol? value) { this.InitializeCommunicationProtocol(); } private void InitializeCommunicationProtocol() { if (this.CommunicationProtocol != null) { this.CommunicationProtocol.ReceivedDataEvent += CommunicationProtocol_ReceivedDataEvent; this.CommunicationProtocol.SentDataEvent += CommunicationProtocol_SentDataEvent; this.CommunicationProtocol.ConnectionStateChangedEvent += CommunicationProtocol_ConnectionStateChangedEvent; } // initializes default newline separators, updates them directly inside ReceivedData and SentData objects this.InitializeNewlineSeparatorsFromAppSettings(); // update newline settings in data objects this.ReceivedData.NewlineSeparator = this.DataDisplayNewlineSeparatorType; this.SentData.NewlineSeparator = this.DataDisplayNewlineSeparatorType; } private void CommunicationProtocol_ReceivedDataEvent(object? sender, ReceivedDataEventArgs e) { // guard null if (this.ReceivedData == null) { throw new NullReferenceException($"{nameof(CommunicationProtocol_ReceivedDataEvent)} found {nameof(this.ReceivedData)} to be null."); } // handover data this.ReceivedData.HandleNewData(e.ReceivedData); } private void CommunicationProtocol_SentDataEvent(object? sender, SentDataEventArgs e) { // guard null if (this.SentData == null) { throw new NullReferenceException($"{nameof(CommunicationProtocol_SentDataEvent)} found {nameof(this.SentData)} to be null."); } // handover data this.SentData.HandleNewData(e.SentData); } private void CommunicationProtocol_ConnectionStateChangedEvent(object? sender, ProtocolConnectionState e) { // update internal state this.CommunicationProtocolConnectionState = e; // If ViewModel still displays connected after unintentional disconnect => force to disconnected state // allows user to change settings if (e == ProtocolConnectionState.UnintentionallyDisconnected && this.ProtocolSettings!.DisplaysConnected == true) { this.ProtocolSettings.ForceConnectedState(false); } } #endregion /// /// Sends the given data to this instances . /// Appends configured to the end of string. /// /// data in form of enumerable (e.g. array) /// when data could be sent successfully protected bool SendToCommunicationProtocol(IEnumerable data) { // guard null values if(this.CommunicationProtocol == null) { throw new NullReferenceException($"'{nameof(SendToCommunicationProtocol)}()' was called but {nameof(CommunicationProtocol)} is null."); } // inform user and quit if communication protocol is not connected if (this.CommunicationProtocol.State != ProtocolConnectionState.Connected) { this.messenger.Send(new ProtocolNotConnectedMessage("Cannot send message, Protocol is not connected.")); return false; } // add newline sequence to end of data byte[] newlineSequence = this.SendNewlineSeparatorType switch { NewlineSeparatorType.None => Array.Empty(), NewlineSeparatorType.CR => new byte[] { (byte)'\r' }, NewlineSeparatorType.LF => new byte[] { (byte)'\n' }, NewlineSeparatorType.CR_LF => new byte[] { (byte)'\r', (byte)'\n' }, _ => throw new NotImplementedException($"'{nameof(SendToCommunicationProtocol)}()' " + $"does not implement handling for {nameof(NewlineSeparatorType)} {this.SendNewlineSeparatorType}.") }; // join data and newline sequence var dataWithNewlineSequence = data.Concat(newlineSequence); // send return this.CommunicationProtocol.SendBytes(dataWithNewlineSequence.ToArray()); } #region Protocol Settings partial void OnProtocolSettingsChanged(IProtocolSettingsViewModel? value) { // register event handler for connection request from viewmodel if (value != null) { this.ProtocolSettings!.ConnectRequested += OnViewModelRequestedConnect; this.ProtocolSettings!.DisconnectRequested += OnViewModelRequestedDisconnect; } } #endregion #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); this.UpdateTitle(); } 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. /// /// true if closing is allowed. false if it shall be cancelled. protected virtual bool ClosingActions() { return true; } [RelayCommand] public void CloseRequest() { // run internal closing actions check if closing is allowed if (this.ClosingActions() == true) { // disconnect communication protocol this.OnViewModelRequestedDisconnect(this, EventArgs.Empty); // raise event ClosingEvent?.Invoke(this, EventArgs.Empty); } } public void ForceClose() { // internal closing options, ignore cancellation this.ClosingActions(); // disconnect communication protocol this.OnViewModelRequestedDisconnect(this, EventArgs.Empty); // do not raise closing event, since caller knows about closing } #endregion #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. /// private void InitializeNewlineSeparatorsFromAppSettings() { // get newline separators from persistent settings, if not available catch exceptions. apply to displayed data variable. this.appSettings.TryReadSetting(defaultReceiveNewlineSeparatorAppSettingsKey, out string settingValueReceiveNLSep); try { this.DataDisplayNewlineSeparatorType = EnumHelpers.ParseEnum(settingValueReceiveNLSep); } catch { } // apply to send newline separator this.appSettings.TryReadSetting(defaultSendNewlineSeparatorAppSettingsKey, out string settingValueSendNLSep); try { this.SendNewlineSeparatorType = EnumHelpers.ParseEnum(settingValueSendNLSep); } catch { } } partial void OnDataDisplayNewlineSeparatorTypeChanged(NewlineSeparatorType value) { // triggers rearranging the displayed data with the new NewlineSeparatorType if (this.ReceivedData != null) { this.ReceivedData.NewlineSeparator = value; } if (this.SentData != null) { this.SentData.NewlineSeparator = value; } } #endregion }