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;
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
///
/// 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;
///
/// as ObservableProperty.
/// Required since does not implement .
///
[ObservableProperty]
private string communicationProtocolLongIdentifier = string.Empty;
#endregion
public TerminalViewModel(IAppSettingsProvider appSettings, IMessenger messenger)
{
this.appSettings = appSettings;
this.messenger = messenger;
}
~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
// TODO!!!
//this.ReceivedData.NewlineSeparator = this.DataDisplayNewlineSeparatorType;
//this.SentData.NewlineSeparator = this.DataDisplayNewlineSeparatorType;
}
public abstract void DisplayNewReceivedData(IEnumerable newData);
private void CommunicationProtocol_ReceivedDataEvent(object? sender, ReceivedDataEventArgs e)
{
// handover data
this.DisplayNewReceivedData(e.ReceivedData);
}
public abstract void DisplayNewSentData(IEnumerable newData);
private void CommunicationProtocol_SentDataEvent(object? sender, SentDataEventArgs e)
{
// handover data
this.DisplayNewSentData(e.SentData);
}
private void CommunicationProtocol_ConnectionStateChangedEvent(object? sender, ProtocolConnectionState e)
{
// update internal state
this.CommunicationProtocolConnectionState = e;
this.CommunicationProtocolLongIdentifier = this.CommunicationProtocol!.LongInstanceIdentifier;
// 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 ProtocolNotConnectedUIMessage("Cannot send message."));
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 { }
}
protected abstract void ActOnDataDisplayNewlineSeparatorTypeChanged(NewlineSeparatorType newType);
partial void OnDataDisplayNewlineSeparatorTypeChanged(NewlineSeparatorType value)
{
this.ActOnDataDisplayNewlineSeparatorTypeChanged(value);
}
#endregion
}