From 1fab46d5c57e967d65b299931b2bd853d767272a Mon Sep 17 00:00:00 2001 From: Jonas Arnold Date: Tue, 25 Apr 2023 10:44:28 +0200 Subject: [PATCH] wired up real communication protocol to the CommunicationDataViewModel, added DisconnectedEvent to CommunicationProtocol, added handling for sent data while not connected, introduced IContext and WpfContext to handover UI context to backend (required to add to ObservableCollection) --- Common/Helpers/ContextHelpers.cs | 21 ++++++ Common/IContext.cs | 24 ++++++ .../Messaging/GenericUserInterfaceMessage.cs | 31 ++++++++ .../Types/ProtocolNotConnectedMessage.cs | 27 +++++++ .../ViewModel/CommunicationDataViewModel.cs | 17 ++++- .../ViewModel/SendReceiveViewModel.cs | 11 ++- MultiTerm.Core/ViewModel/TerminalViewModel.cs | 74 +++++++++++++++---- MultiTerm.Protocols/CommunicationProtocol.cs | 57 ++++++++++++-- MultiTerm.Protocols/DisconnectedEventArgs.cs | 16 ++++ MultiTerm.Protocols/ICommunicationProtocol.cs | 15 ++++ MultiTerm.Protocols/Serial/SerialProtocol.cs | 18 ++++- .../DataViewModelToStringConverter.cs | 3 + MultiTerm.Wpf/App.xaml.cs | 2 + MultiTerm.Wpf/WpfContext.cs | 54 ++++++++++++++ 14 files changed, 339 insertions(+), 31 deletions(-) create mode 100644 Common/Helpers/ContextHelpers.cs create mode 100644 Common/IContext.cs create mode 100644 Common/Messaging/GenericUserInterfaceMessage.cs create mode 100644 MultiTerm.Core/Types/ProtocolNotConnectedMessage.cs create mode 100644 MultiTerm.Protocols/DisconnectedEventArgs.cs create mode 100644 MultiTerm.Wpf/WpfContext.cs diff --git a/Common/Helpers/ContextHelpers.cs b/Common/Helpers/ContextHelpers.cs new file mode 100644 index 0000000..2fea4bf --- /dev/null +++ b/Common/Helpers/ContextHelpers.cs @@ -0,0 +1,21 @@ +namespace Common.Helpers; + +public static class ContextHelpers +{ + /// + /// Invokes context if necessary and runs action. If Threads are synchronized, runs action without switching context. + /// + /// context to run action on + /// action to run + public static void InvokeIfNecessary(IContext context, Action action) + { + if (context.IsSynchronized == false) + { + context.Invoke(action); + } + else + { + action.Invoke(); + } + } +} diff --git a/Common/IContext.cs b/Common/IContext.cs new file mode 100644 index 0000000..0b1471f --- /dev/null +++ b/Common/IContext.cs @@ -0,0 +1,24 @@ +namespace Common; + +/// +/// Provides Context of some thread. Allows user of the interface to run actions on the thread of the implementer. +/// +public interface IContext +{ + /// + /// Indicates wether the thread this property gotten from is equal to the implementers thread. + /// + bool IsSynchronized { get; } + + /// + /// Run on the implementers thread. + /// + /// action to run + void Invoke(Action action); + + /// + /// Start running on the implementers thread. + /// + /// action to start + void BeginInvoke(Action action); +} diff --git a/Common/Messaging/GenericUserInterfaceMessage.cs b/Common/Messaging/GenericUserInterfaceMessage.cs new file mode 100644 index 0000000..abfeb4d --- /dev/null +++ b/Common/Messaging/GenericUserInterfaceMessage.cs @@ -0,0 +1,31 @@ +namespace Common.Messaging; + +public class GenericUserInterfaceMessage : IUserInterfaceMessage +{ + /// + /// Description of the Message. Containes messsage text. + /// + public string Message { get; private set; } + + /// + /// Importance of this message. + /// + public MessageImportance Importance { get; private set; } + + public GenericUserInterfaceMessage(string message, MessageImportance importance) + { + // throw when message is empty + if (string.IsNullOrEmpty(message)) + { + throw new NotImplementedException($"Please provide a valid message for the {nameof(GenericUserInterfaceMessage)}"); + } + + this.Message = message; + this.Importance = importance; + } + + public override string ToString() + { + return this.Message; + } +} diff --git a/MultiTerm.Core/Types/ProtocolNotConnectedMessage.cs b/MultiTerm.Core/Types/ProtocolNotConnectedMessage.cs new file mode 100644 index 0000000..d1e3da3 --- /dev/null +++ b/MultiTerm.Core/Types/ProtocolNotConnectedMessage.cs @@ -0,0 +1,27 @@ +using Common.Messaging; + +namespace MultiTerm.Core.Types; + +public class ProtocolNotConnectedMessage : IUserInterfaceMessage +{ + private readonly string reason; + + public MessageImportance Importance => MessageImportance.Medium; + + public ProtocolNotConnectedMessage(string reason) + { + this.reason = reason; + } + + public override string ToString() + { + if (string.IsNullOrEmpty(reason)) + { + return $"Protocol is not connected."; + } + else + { + return $"{reason}: Protocol is not connected."; + } + } +} diff --git a/MultiTerm.Core/ViewModel/CommunicationDataViewModel.cs b/MultiTerm.Core/ViewModel/CommunicationDataViewModel.cs index d16682a..0330a9f 100644 --- a/MultiTerm.Core/ViewModel/CommunicationDataViewModel.cs +++ b/MultiTerm.Core/ViewModel/CommunicationDataViewModel.cs @@ -1,15 +1,19 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using Common; +using Common.Helpers; +using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using MultiTerm.Core.Types; using MultiTerm.Protocols; using MultiTerm.Protocols.Model; using System.Collections.ObjectModel; + namespace MultiTerm.Core.ViewModel; public partial class CommunicationDataViewModel : ObservableObject { private ICommunicationProtocol? communicationProtocol; + private readonly IContext uiContext; private NewlineSeparatorType currentReceiveNewlineSeparatorType = NewlineSeparatorType.None; private NewlineSeparatorType currentSentNewlineSeparatorType = NewlineSeparatorType.None; @@ -37,8 +41,9 @@ public partial class CommunicationDataViewModel : ObservableObject [ObservableProperty] private List? selectedSentData; - public CommunicationDataViewModel(ICommunicationProtocol? communicationProtocol) + public CommunicationDataViewModel(ICommunicationProtocol? communicationProtocol, IContext context) { + this.uiContext = context; this.communicationProtocol = communicationProtocol; if (this.communicationProtocol != null) { @@ -261,8 +266,12 @@ public partial class CommunicationDataViewModel : ObservableObject // go through every character foreach (var newExtdChar in characters) { - // add to collection with the current counter - dataCollection.Add(new DataViewModel(newExtdChar, collectionLineCounter)); + // add to collection with the current counter, invoking UI context if necssary + var currentLineCounter = collectionLineCounter; + ContextHelpers.InvokeIfNecessary(this.uiContext, (Action)delegate + { + dataCollection.Add(new DataViewModel(newExtdChar, currentLineCounter)); + }); switch (ShouldIntroduceNewlineAfterThisCharacter(newExtdChar.Character, previousCharacters, newlineSeparatorType)) { diff --git a/MultiTerm.Core/ViewModel/SendReceiveViewModel.cs b/MultiTerm.Core/ViewModel/SendReceiveViewModel.cs index f0d99dd..de43187 100644 --- a/MultiTerm.Core/ViewModel/SendReceiveViewModel.cs +++ b/MultiTerm.Core/ViewModel/SendReceiveViewModel.cs @@ -1,6 +1,8 @@ -using Common.AppSettings; +using Common; +using Common.AppSettings; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; using MultiTerm.Core.Model; using MultiTerm.Core.Types; using System.Diagnostics; @@ -35,7 +37,7 @@ public partial class SendReceiveViewModel : TerminalViewModel /// Constructor. /// /// - public SendReceiveViewModel(IAppSettingsProvider appSettings) : base(appSettings) { } + public SendReceiveViewModel(IAppSettingsProvider appSettings, IMessenger messenger, IContext context) : base(appSettings, messenger, context) { } /// /// Send command. @@ -47,5 +49,10 @@ public partial class SendReceiveViewModel : TerminalViewModel var items = this.CommunicationData.SelectedReceivedData; Debugger.Break(); this.SentData = this.SendData.ToString(); + // send data + this.SendToCommunicationProtocol(this.SendData.ToString()); + + // clear textbox + this.SendData = new MultiFormatString(); } } diff --git a/MultiTerm.Core/ViewModel/TerminalViewModel.cs b/MultiTerm.Core/ViewModel/TerminalViewModel.cs index 1ef9dd8..7cc1c88 100644 --- a/MultiTerm.Core/ViewModel/TerminalViewModel.cs +++ b/MultiTerm.Core/ViewModel/TerminalViewModel.cs @@ -1,10 +1,14 @@ -using Common.AppSettings; +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.Types; +using System.Text; namespace MultiTerm.Core.ViewModel; @@ -13,6 +17,8 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie private const string defaultReceiveNewlineSeparatorAppSettingsKey = "DefaultReceiveNewlineSeparator"; private const string defaultSendNewlineSeparatorAppSettingsKey = "DefaultSendNewlineSeparator"; private readonly IAppSettingsProvider appSettings; + private readonly IMessenger messenger; + private readonly IContext context; public abstract string Title { get; } public abstract TerminalViewType ViewType { get; } @@ -22,7 +28,7 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie /// Holds communication data, meaning data that was sent to or received over the communication protocol. /// [ObservableProperty] - private CommunicationDataViewModel communicationData = new(null); + private CommunicationDataViewModel? communicationData; /// /// Newline Separator Type that is selected for receival data of this Terminal. Defaults to none. @@ -39,12 +45,15 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie private ICommunicationProtocol? communicationProtocol; public ICommunicationProtocol? CommunicationProtocol { - get { return communicationProtocol; } + get { return this.communicationProtocol; } set { - communicationProtocol = value; + this.communicationProtocol = value; // register communication protocol in the Communication Data View Model - //this.CommunicationData = new CommunicationDataViewModel(communicationProtocol); // TEMP + this.CommunicationData = new CommunicationDataViewModel(this.communicationProtocol, this.context); + + // initializes default newline separators, requires communicationData to be not null + this.InitializeNewlineSeparatorsFromAppSettings(); // initialize both newline Separators. initializes with null if they are not available. this.CommunicationData.ConfigureNewlineSeparators(this.ReceiveNewlineSeparatorType, this.SendNewlineSeparatorType); @@ -54,23 +63,60 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie private IProtocolSettingsViewModel? protocolSettings; public IProtocolSettingsViewModel? ProtocolSettings { - get { return protocolSettings; } + get { return this.protocolSettings; } set - { - protocolSettings = value; + { + this.protocolSettings = value; // register event handler for connection request from viewmodel if(value != null) { - protocolSettings!.ConnectRequested += OnViewModelRequestedConnect; - protocolSettings!.DisconnectRequested += OnViewModelRequestedDisconnect; + this.protocolSettings!.ConnectRequested += OnViewModelRequestedConnect; + this.protocolSettings!.DisconnectRequested += OnViewModelRequestedDisconnect; } } } - public TerminalViewModel(IAppSettingsProvider appSettings) + public TerminalViewModel(IAppSettingsProvider appSettings, IMessenger messenger, IContext context) { this.appSettings = appSettings; - this.InitializeNewlineSeparatorsFromAppSettings(); + this.messenger = messenger; + this.context = context; + } + + /// + /// Sends the given string to this instances , UTF-16 encoded. + /// Appends configured to the end of string. + /// + /// data string + protected void SendToCommunicationProtocol(string data) + { + // guard communication protocol null + 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.IsConnected == false) + { + this.messenger.Send(new ProtocolNotConnectedMessage("Cannot send message")); + return; + } + + // add newline sequence to end of data + string newlineSequence = this.SendNewlineSeparatorType switch + { + NewlineSeparatorType.None => String.Empty, + NewlineSeparatorType.CR => "\r", + NewlineSeparatorType.LF => "\n", + NewlineSeparatorType.CR_LF => "\r\n", + _ => throw new NotImplementedException($"'{nameof(SendToCommunicationProtocol)}()' " + + $"does not implement handling for {nameof(NewlineSeparatorType)} {this.SendNewlineSeparatorType}.") + }; + string modifiedData = data + newlineSequence; + + // extract unicode byte array + var byteArray = Encoding.Unicode.GetBytes(modifiedData); + + // send + this.CommunicationProtocol.SendBytes(byteArray); } #region Connect/Disconnect @@ -141,13 +187,13 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie partial void OnReceiveNewlineSeparatorTypeChanged(NewlineSeparatorType value) { // triggers rearranging the received data with the new NewlineSeparatorType - this.CommunicationData.ConfigureNewlineSeparators(value, null); + this.CommunicationData?.ConfigureNewlineSeparators(value, null); } partial void OnSendNewlineSeparatorTypeChanged(NewlineSeparatorType value) { // triggers rearranging the sent data with the new NewlineSeparatorType - this.CommunicationData.ConfigureNewlineSeparators(null, value); + this.CommunicationData?.ConfigureNewlineSeparators(null, value); } #endregion diff --git a/MultiTerm.Protocols/CommunicationProtocol.cs b/MultiTerm.Protocols/CommunicationProtocol.cs index 68b5e27..b63592a 100644 --- a/MultiTerm.Protocols/CommunicationProtocol.cs +++ b/MultiTerm.Protocols/CommunicationProtocol.cs @@ -1,4 +1,5 @@ using Common.Logging; +using Common.Messaging; using CommunityToolkit.Mvvm.Messaging; using MultiTerm.Protocols.Types; @@ -6,43 +7,72 @@ namespace MultiTerm.Protocols; public abstract class CommunicationProtocol : ICommunicationProtocol { - protected ILogger logger; + protected readonly ILogger logger; + protected readonly IMessenger messenger; private CancellationTokenSource cancellationTokenSource; private Thread? readingThread; public event EventHandler? ReceivedDataEvent; public event EventHandler? SentDataEvent; + public event EventHandler? DisconnectedEvent; public abstract ProtocolType ProtocolType { get; } - public CommunicationProtocol(ILogger logger) + public bool IsConnected { get; private set; } = false; + + public CommunicationProtocol(ILogger logger, IMessenger messenger) { // initialize other this.cancellationTokenSource = new CancellationTokenSource(); this.logger = logger; + this.messenger = messenger; } /// /// To be called when data was received from the connected device. /// - /// protected void OnReceivedData(ReceivedDataEventArgs e) { this.ReceivedDataEvent?.Invoke(this, e); } /// /// To be called when data was sent to the connected device. /// - /// protected void OnSentData(SentDataEventArgs e) { this.SentDataEvent?.Invoke(this, e); } + /// + /// To be called whenever the protocol detected that it is disconnected, but this is not a known fact. + /// + protected void OnUnintentionallyDisconnected() + { + // update state + this.IsConnected = false; + // log + this.logger.LogError($"'{nameof(OnUnintentionallyDisconnected)}()' called.", nameof(CommunicationProtocol)); + // raise event indicating an unintentional disconnect + this.DisconnectedEvent?.Invoke(this, new DisconnectedEventArgs(true)); + } + /// /// Allows to send bytes using the implemented protocol. /// /// data as bytes - protected abstract void InternalSendBytes(byte[] bytes); + /// true if the data was sent successfully + protected abstract bool InternalSendBytes(byte[] bytes); public void SendBytes(byte[] bytes) { - this.InternalSendBytes(bytes); + // guard is not connected => log warning. user of this function shall only use SendBytes if IsConnected is true + if (this.IsConnected == false) { this.logger.LogWarn($"'{nameof(SendBytes)}()' was reached with {nameof(IsConnected)} being false", nameof(CommunicationProtocol)); } + + if (this.InternalSendBytes(bytes)) + { + // todo implement sent bytes + // ONsentBytes() + } + else + { + this.logger.LogError($"'{nameof(SendBytes)}()' failed to send during {nameof(InternalSendBytes)}.", nameof(CommunicationProtocol)); + this.messenger.Send(new GenericUserInterfaceMessage("Failed to send message", MessageImportance.High)); + } } /// @@ -61,6 +91,13 @@ public abstract class CommunicationProtocol : ICommunicationProtocol public bool Connect(IProtocolSettings settings) { + // check if not already connected + if (this.IsConnected == true) + { + this.logger.LogWarn($"'{nameof(Connect)}()' was reached even if {nameof(IsConnected)} is already true.", nameof(CommunicationProtocol)); + return true; + } + // check if settings are valid, cancel if not if (settings.AreValid() == false) { @@ -68,7 +105,7 @@ public abstract class CommunicationProtocol : ICommunicationProtocol return false; } - // try connecting if serttings are valid + // try connecting if settings are valid if (this.InternalConnect(settings)) { // renew token source @@ -76,6 +113,8 @@ public abstract class CommunicationProtocol : ICommunicationProtocol // start internal reading thread this.readingThread = new Thread(() => this.InternalRead(this.cancellationTokenSource.Token)); this.readingThread.Start(); + // update state + this.IsConnected = true; return true; } else @@ -98,5 +137,9 @@ public abstract class CommunicationProtocol : ICommunicationProtocol this.readingThread.Join(); } this.InternalDisconnect(); + this.IsConnected = false; + + // raise event indicating an intentional disconnect + this.DisconnectedEvent?.Invoke(this, new DisconnectedEventArgs(false)); } } diff --git a/MultiTerm.Protocols/DisconnectedEventArgs.cs b/MultiTerm.Protocols/DisconnectedEventArgs.cs new file mode 100644 index 0000000..420c2a5 --- /dev/null +++ b/MultiTerm.Protocols/DisconnectedEventArgs.cs @@ -0,0 +1,16 @@ +namespace MultiTerm.Protocols; + +public class DisconnectedEventArgs : EventArgs +{ + /// + /// Indicates wether the disconnect was unintentional. + /// It False the disconnect was likely triggered by a user (e.g. manually clicking Disconnect). + /// If True the disconnect was unintentional. + /// + public bool Unintentional { get; private set; } + + public DisconnectedEventArgs(bool unintentional) + { + this.Unintentional = unintentional; + } +} diff --git a/MultiTerm.Protocols/ICommunicationProtocol.cs b/MultiTerm.Protocols/ICommunicationProtocol.cs index c0c42f5..5ab2c65 100644 --- a/MultiTerm.Protocols/ICommunicationProtocol.cs +++ b/MultiTerm.Protocols/ICommunicationProtocol.cs @@ -22,10 +22,25 @@ public interface ICommunicationProtocol /// event EventHandler? SentDataEvent; + /// + /// Thrown when the device disconnected. + /// + event EventHandler? DisconnectedEvent; + + /// + /// Indicates wether the communication protocol is connected. + /// True if connected and ready to receive data via , false if not. + /// + bool IsConnected { get; } + /// /// Connect to the device. /// /// settings required to connect and use the protocol + /// + /// true if connected sucessfully or already connected, + /// false if the provided settings are invalid or connection was unsuccessful + /// bool Connect(IProtocolSettings settings); /// diff --git a/MultiTerm.Protocols/Serial/SerialProtocol.cs b/MultiTerm.Protocols/Serial/SerialProtocol.cs index d85515a..4d45227 100644 --- a/MultiTerm.Protocols/Serial/SerialProtocol.cs +++ b/MultiTerm.Protocols/Serial/SerialProtocol.cs @@ -1,4 +1,5 @@ using Common.Logging; +using CommunityToolkit.Mvvm.Messaging; using MultiTerm.Protocols.Model; using MultiTerm.Protocols.Types; using RJCP.IO.Ports; @@ -13,7 +14,7 @@ public class SerialProtocol : CommunicationProtocol private SerialPortStream serialPort = new(); - public SerialProtocol(ILogger logger) : base(logger) { } + public SerialProtocol(ILogger logger, IMessenger messenger) : base(logger, messenger) { } protected override bool InternalConnect(IProtocolSettings protocolSettings) { @@ -40,7 +41,7 @@ public class SerialProtocol : CommunicationProtocol Handshake = new HandshakeLibraryEquivalentConverter().ConvertToLibraryType(this.serialSettings.Handshake), // define static settings - Encoding = Encoding.Unicode, + Encoding = Encoding.ASCII, ReadTimeout = 500, WriteTimeout = 500 }; @@ -81,12 +82,21 @@ public class SerialProtocol : CommunicationProtocol } } - protected override void InternalSendBytes(byte[] bytes) + protected override bool InternalSendBytes(byte[] bytes) { foreach (byte b in bytes) { - serialPort.WriteByte(b); + try + { + serialPort.WriteByte(b); + } + // When the Serial Port is closed and InvalidOperationException is thrown + catch(InvalidOperationException) + { + this.OnUnintentionallyDisconnected(); + } } + return true; } public static IEnumerable GetPortNames() diff --git a/MultiTerm.Wpf.CustomControl/ValueConverter/DataViewModelToStringConverter.cs b/MultiTerm.Wpf.CustomControl/ValueConverter/DataViewModelToStringConverter.cs index 375ca5d..f90e32d 100644 --- a/MultiTerm.Wpf.CustomControl/ValueConverter/DataViewModelToStringConverter.cs +++ b/MultiTerm.Wpf.CustomControl/ValueConverter/DataViewModelToStringConverter.cs @@ -33,6 +33,9 @@ public class DataViewModelToStringConverter : IValueConverter // guard type if (value is not IEnumerable enumerable) { throw new ArgumentException($"Can only convert from {nameof(IEnumerable)} value"); } + // guard empty enumerable + if (enumerable.Count() == 0) { return string.Empty; } + string returnString = string.Empty; // iterate through data, adding content to textbox diff --git a/MultiTerm.Wpf/App.xaml.cs b/MultiTerm.Wpf/App.xaml.cs index e0fbf58..0ec00b8 100644 --- a/MultiTerm.Wpf/App.xaml.cs +++ b/MultiTerm.Wpf/App.xaml.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Common.AppSettings; using MultiTerm.Protocols.Helpers; using CommunityToolkit.Mvvm.Messaging; +using Common; namespace MultiTerm.Wpf; @@ -25,6 +26,7 @@ public partial class App : Application .ConfigureServices((hostContext, services) => { services.AddSingleton(); + services.AddSingleton(new WpfContext()); services.AddSingleton(new SerilogLogger("C:/log/multiterm-log-.txt", true)); services.AddSingleton(new XmlAppSettingsProvider("C:/log/multiterm-config.xml")); services.AddSingleton(); diff --git a/MultiTerm.Wpf/WpfContext.cs b/MultiTerm.Wpf/WpfContext.cs new file mode 100644 index 0000000..446ea9c --- /dev/null +++ b/MultiTerm.Wpf/WpfContext.cs @@ -0,0 +1,54 @@ +using Common; +using System; +using System.Threading; +using System.Windows.Threading; + +namespace MultiTerm.Wpf; + +/// +/// Provides an implementation of the for WPF. +/// +public class WpfContext : IContext +{ + private readonly Dispatcher dispatcher; + + public bool IsSynchronized + { + get + { + return this.dispatcher.Thread == Thread.CurrentThread; + } + } + + /// + /// Instanciates a using the CurrentDispatcher. + /// + public WpfContext() : this(Dispatcher.CurrentDispatcher) { } + + /// + /// Instanciates a using the given . + /// + public WpfContext(Dispatcher dispatcher) + { + // guard null + if(dispatcher == null) { throw new ArgumentNullException(nameof(dispatcher)); } + + this.dispatcher = dispatcher; + } + + public void BeginInvoke(Action action) + { + // guard null + if (action == null) { throw new ArgumentNullException(nameof(action)); } + + this.dispatcher.BeginInvoke(action); + } + + public void Invoke(Action action) + { + // guard null + if (action == null) { throw new ArgumentNullException(nameof(action)); } + + this.dispatcher.Invoke(action); + } +}