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);
+ }
+}