diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0eeeaa4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +[*.cs] + +# IDE0011: Add braces +csharp_prefer_braces = when_multiline +csharp_style_var_for_built_in_types=true:silent +csharp_style_var_when_type_is_apparent=true:silent +csharp_style_var_elsewhere=true:silent + +dotnet_diagnostic.IDE0055.severity = none + +dotnet_diagnostic.IDE0130.severity = none \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7964536..75ac72d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,189 +1,5 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo +.VS *.user -*.sln.docstates -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -x64/ -build/ -bld/ -[Bb]in/ -[Oo]bj/ - -# Roslyn cache directories -*.ide/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -#NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opensdf -*.sdf -*.cachefile - -# Visual Studio profiler -*.psess -*.vsp -*.vspx - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding addin-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -## TODO: Comment the next line if you want to checkin your -## web deploy settings but do note that will include unencrypted -## passwords -#*.pubxml - -# NuGet Packages Directory -packages/* -## TODO: If the tool you use requires repositories.config -## uncomment the next line -#!packages/repositories.config - -# Enable "build/" folder in the NuGet Packages folder since -# NuGet packages use it for MSBuild targets. -# This line needs to be after the ignore of the build folder -# (and the packages folder if the line above has been uncommented) -!packages/build/ - -# Windows Azure Build Output -csx/ -*.build.csdef - -# Windows Store app package directory -AppPackages/ - -# Others -sql/ -*.Cache -ClientBin/ -[Ss]tyle[Cc]op.* -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.pfx -*.publishsettings -node_modules/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# LightSwitch generated files -GeneratedArtifacts/ -_Pvt_Extensions/ -ModelManifest.xml \ No newline at end of file +bin +obj \ No newline at end of file diff --git a/App.config b/App.config deleted file mode 100644 index 8e15646..0000000 --- a/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Icons.xaml b/Icons.xaml deleted file mode 100644 index 848e067..0000000 --- a/Icons.xaml +++ /dev/null @@ -1,483 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Images/Clear.xaml b/Images/Clear.xaml deleted file mode 100644 index 85ad8ee..0000000 --- a/Images/Clear.xaml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Images/Connect.xaml b/Images/Connect.xaml deleted file mode 100644 index a31ac6f..0000000 --- a/Images/Connect.xaml +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Images/Disconnect.xaml b/Images/Disconnect.xaml deleted file mode 100644 index 66d8822..0000000 --- a/Images/Disconnect.xaml +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Infrastructure/BaseViewModel.cs b/Infrastructure/BaseViewModel.cs deleted file mode 100644 index 152d71b..0000000 --- a/Infrastructure/BaseViewModel.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.ComponentModel; -using System.Runtime.CompilerServices; - -namespace PettingZoo.Infrastructure -{ - public class BaseViewModel : INotifyPropertyChanged - { - public event PropertyChangedEventHandler PropertyChanged; - - protected virtual void RaisePropertyChanged([CallerMemberName] string propertyName = null) - { - var handler = PropertyChanged; - if (handler != null) - handler(this, new PropertyChangedEventArgs(propertyName)); - } - - - protected virtual void RaiseOtherPropertyChanged(string propertyName) - { - var handler = PropertyChanged; - if (handler != null) - handler(this, new PropertyChangedEventArgs(propertyName)); - } - } -} diff --git a/Infrastructure/DelegateCommand.cs b/Infrastructure/DelegateCommand.cs deleted file mode 100644 index f4eb492..0000000 --- a/Infrastructure/DelegateCommand.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Windows.Input; - -namespace PettingZoo.Infrastructure -{ - public class DelegateCommand : ICommand - { - private readonly Func canExecute; - private readonly Action execute; - - public event EventHandler CanExecuteChanged; - - - public DelegateCommand(Action execute) : this(execute, null) - { - } - - public DelegateCommand(Action execute, Func canExecute) - { - this.execute = execute; - this.canExecute = canExecute; - } - - - public bool CanExecute(object parameter) - { - return canExecute == null || canExecute((T)parameter); - } - - - public void Execute(object parameter) - { - execute((T)parameter); - } - - - public void RaiseCanExecuteChanged() - { - if (CanExecuteChanged != null) - CanExecuteChanged(this, EventArgs.Empty); - } - } - - - - public class DelegateCommand : ICommand - { - private readonly Func canExecute; - private readonly Action execute; - - public event EventHandler CanExecuteChanged; - - - public DelegateCommand(Action execute) : this(execute, null) { } - - public DelegateCommand(Action execute, Func canExecute) - { - this.execute = execute; - this.canExecute = canExecute; - } - - - public bool CanExecute(object parameter) - { - return canExecute == null || canExecute(); - } - - - public void Execute(object parameter) - { - execute(); - } - - - public void RaiseCanExecuteChanged() - { - if (CanExecuteChanged != null) - CanExecuteChanged(this, EventArgs.Empty); - } - } -} diff --git a/Model/ConnectionInfo.cs b/Model/ConnectionInfo.cs deleted file mode 100644 index 8ba3ba4..0000000 --- a/Model/ConnectionInfo.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace PettingZoo.Model -{ - public class ConnectionInfo - { - public string Host { get; set; } - public string VirtualHost { get; set; } - public int Port { get; set; } - public string Username { get; set; } - public string Password { get; set; } - - public string Exchange { get; set; } - public string RoutingKey { get; set; } - } -} diff --git a/Model/IConnection.cs b/Model/IConnection.cs deleted file mode 100644 index 394a721..0000000 --- a/Model/IConnection.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; - -namespace PettingZoo.Model -{ - public enum ConnectionStatus - { - Disconnected, - Connecting, - Connected, - Error - } - - - public class StatusChangedEventArgs : EventArgs - { - public ConnectionStatus Status { get; private set; } - public string Context { get; private set; } - - - public StatusChangedEventArgs(ConnectionStatus status, string context) - { - Status = status; - Context = context; - } - } - - - public class MessageReceivedEventArgs : EventArgs - { - public MessageInfo MessageInfo { get; private set; } - - - public MessageReceivedEventArgs(MessageInfo messageInfo) - { - MessageInfo = messageInfo; - } - } - - - - public interface IConnection : IDisposable - { - event EventHandler StatusChanged; - event EventHandler MessageReceived; - } -} diff --git a/Model/IConnectionFactory.cs b/Model/IConnectionFactory.cs deleted file mode 100644 index e1b6bd5..0000000 --- a/Model/IConnectionFactory.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace PettingZoo.Model -{ - public interface IConnectionFactory - { - IConnection CreateConnection(ConnectionInfo connectionInfo); - } -} diff --git a/Model/IConnectionInfoBuilder.cs b/Model/IConnectionInfoBuilder.cs deleted file mode 100644 index bdd683c..0000000 --- a/Model/IConnectionInfoBuilder.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace PettingZoo.Model -{ - public interface IConnectionInfoBuilder - { - ConnectionInfo Build(); - } -} diff --git a/Model/MessageInfo.cs b/Model/MessageInfo.cs deleted file mode 100644 index 7a29779..0000000 --- a/Model/MessageInfo.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace PettingZoo.Model -{ - public class MessageInfo - { - public DateTime Timestamp { get; set; } - public string Exchange { get; set; } - public string RoutingKey { get; set; } - public byte[] Body { get; set; } - - public Dictionary Properties; - - public string ContentType - { - get - { - return Properties != null && Properties.ContainsKey(RabbitMQProperties.ContentType) - ? Properties[RabbitMQProperties.ContentType] - : ""; - } - } - - - public MessageInfo() - { - Timestamp = DateTime.Now; - } - } -} diff --git a/Model/RabbitMQClientConnection.cs b/Model/RabbitMQClientConnection.cs deleted file mode 100644 index 0cd573d..0000000 --- a/Model/RabbitMQClientConnection.cs +++ /dev/null @@ -1,190 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using PettingZoo.Properties; -using RabbitMQ.Client; -using RabbitMQ.Client.Events; - -namespace PettingZoo.Model -{ - public class RabbitMQClientConnection : IConnection - { - private const int ConnectRetryDelay = 5000; - - private readonly CancellationTokenSource connectionTaskToken; - private RabbitMQ.Client.IConnection connection; - private IModel model; - - - public event EventHandler StatusChanged; - public event EventHandler MessageReceived; - - - public RabbitMQClientConnection(ConnectionInfo connectionInfo) - { - connectionTaskToken = new CancellationTokenSource(); - var connectionToken = connectionTaskToken.Token; - - Task.Factory.StartNew(() => TryConnection(connectionInfo, connectionToken), connectionToken); - } - - - public void Dispose() - { - connectionTaskToken.Cancel(); - - if (model != null) - { - model.Dispose(); - model = null; - } - - if (connection != null) - { - connection.Dispose(); - connection = null; - } - - StatusChanged = null; - MessageReceived = null; - } - - - private void TryConnection(ConnectionInfo connectionInfo, CancellationToken cancellationToken) - { - var factory = new ConnectionFactory - { - HostName = connectionInfo.Host, - Port = connectionInfo.Port, - VirtualHost = connectionInfo.VirtualHost, - UserName = connectionInfo.Username, - Password = connectionInfo.Password - }; - - var statusContext = String.Format("{0}:{1}{2}", connectionInfo.Host, connectionInfo.Port, connectionInfo.VirtualHost); - - while (!cancellationToken.IsCancellationRequested) - { - DoStatusChanged(ConnectionStatus.Connecting, statusContext); - try - { - connection = factory.CreateConnection(); - model = connection.CreateModel(); - - var queueName = model.QueueDeclare().QueueName; - model.QueueBind(queueName, connectionInfo.Exchange, connectionInfo.RoutingKey); - - - var consumer = new EventingBasicConsumer(model); - consumer.Received += ClientReceived; - - model.BasicConsume(queueName, true, consumer); - DoStatusChanged(ConnectionStatus.Connected, statusContext); - - break; - } - catch (Exception e) - { - DoStatusChanged(ConnectionStatus.Error, e.Message); - Task.Delay(ConnectRetryDelay, cancellationToken).Wait(cancellationToken); - } - } - } - - - private void ClientReceived(object sender, BasicDeliverEventArgs args) - { - if (MessageReceived == null) - return; - - MessageReceived(this, new MessageReceivedEventArgs( - new MessageInfo - { - Exchange = args.Exchange, - RoutingKey = args.RoutingKey, - Body = args.Body, - Properties = ConvertProperties(args.BasicProperties) - } - )); - } - - - private void DoStatusChanged(ConnectionStatus status, string context = null) - { - if (StatusChanged != null) - StatusChanged(this, new StatusChangedEventArgs(status, context)); - } - - - private static Dictionary ConvertProperties(IBasicProperties basicProperties) - { - var properties = new Dictionary(); - - if (basicProperties.IsDeliveryModePresent()) - { - string deliveryMode; - - switch (basicProperties.DeliveryMode) - { - case 1: - deliveryMode = Resources.DeliveryModeNonPersistent; - break; - - case 2: - deliveryMode = Resources.DeliveryModePersistent; - break; - - default: - deliveryMode = basicProperties.DeliveryMode.ToString(CultureInfo.InvariantCulture); - break; - } - - properties.Add(RabbitMQProperties.DeliveryMode, deliveryMode); - } - - if (basicProperties.IsContentTypePresent()) - properties.Add(RabbitMQProperties.ContentType, basicProperties.ContentType); - - if (basicProperties.IsContentEncodingPresent()) - properties.Add(RabbitMQProperties.ContentEncoding, basicProperties.ContentEncoding); - - if (basicProperties.IsPriorityPresent()) - properties.Add(RabbitMQProperties.Priority, basicProperties.Priority.ToString(CultureInfo.InvariantCulture)); - - if (basicProperties.IsCorrelationIdPresent()) - properties.Add(RabbitMQProperties.Priority, basicProperties.CorrelationId); - - if (basicProperties.IsReplyToPresent()) - properties.Add(RabbitMQProperties.ReplyTo, basicProperties.ReplyTo); - - if (basicProperties.IsExpirationPresent()) - properties.Add(RabbitMQProperties.Expiration, basicProperties.Expiration); - - if (basicProperties.IsMessageIdPresent()) - properties.Add(RabbitMQProperties.MessageId, basicProperties.MessageId); - - if (basicProperties.IsTimestampPresent()) - properties.Add(RabbitMQProperties.Timestamp, basicProperties.Timestamp.UnixTime.ToString(CultureInfo.InvariantCulture)); - - if (basicProperties.IsTypePresent()) - properties.Add(RabbitMQProperties.Type, basicProperties.Type); - - if (basicProperties.IsUserIdPresent()) - properties.Add(RabbitMQProperties.UserId, basicProperties.UserId); - - if (basicProperties.IsAppIdPresent()) - properties.Add(RabbitMQProperties.UserId, basicProperties.AppId); - - if (basicProperties.IsClusterIdPresent()) - properties.Add(RabbitMQProperties.ClusterId, basicProperties.ClusterId); - - foreach (var header in basicProperties.Headers) - properties.Add(header.Key, Encoding.UTF8.GetString((byte[])header.Value)); - - return properties; - } - } -} diff --git a/Model/RabbitMQClientConnectionFactory.cs b/Model/RabbitMQClientConnectionFactory.cs deleted file mode 100644 index d61aa3e..0000000 --- a/Model/RabbitMQClientConnectionFactory.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace PettingZoo.Model -{ - public class RabbitMQClientConnectionFactory : IConnectionFactory - { - public IConnection CreateConnection(ConnectionInfo connectionInfo) - { - return new RabbitMQClientConnection(connectionInfo); - } - } -} diff --git a/PettingZoo.Core/Connection/ConnectionParams.cs b/PettingZoo.Core/Connection/ConnectionParams.cs new file mode 100644 index 0000000..fb63f5e --- /dev/null +++ b/PettingZoo.Core/Connection/ConnectionParams.cs @@ -0,0 +1,21 @@ +namespace PettingZoo.Core.Connection +{ + public class ConnectionParams + { + public string Host { get; } + public string VirtualHost { get; } + public int Port { get; } + public string Username { get; } + public string Password { get; } + + + public ConnectionParams(string host, string virtualHost, int port, string username, string password) + { + Host = host; + VirtualHost = virtualHost; + Port = port; + Username = username; + Password = password; + } + } +} diff --git a/PettingZoo.Core/Connection/IConnection.cs b/PettingZoo.Core/Connection/IConnection.cs new file mode 100644 index 0000000..f29014c --- /dev/null +++ b/PettingZoo.Core/Connection/IConnection.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; + +namespace PettingZoo.Core.Connection +{ + public interface IConnection : IAsyncDisposable + { + event EventHandler StatusChanged; + + ISubscriber Subscribe(string exchange, string routingKey); + Task Publish(MessageInfo messageInfo); + } + + + public enum ConnectionStatus + { + Disconnected, + Connecting, + Connected, + Error + } + + + public class StatusChangedEventArgs : EventArgs + { + public ConnectionStatus Status { get; } + public string? Context { get; } + + + public StatusChangedEventArgs(ConnectionStatus status, string? context) + { + Status = status; + Context = context; + } + } +} diff --git a/PettingZoo.Core/Connection/IConnectionFactory.cs b/PettingZoo.Core/Connection/IConnectionFactory.cs new file mode 100644 index 0000000..a2f7170 --- /dev/null +++ b/PettingZoo.Core/Connection/IConnectionFactory.cs @@ -0,0 +1,7 @@ +namespace PettingZoo.Core.Connection +{ + public interface IConnectionFactory + { + IConnection CreateConnection(ConnectionParams connectionInfo); + } +} diff --git a/PettingZoo.Core/Connection/ISubscriber.cs b/PettingZoo.Core/Connection/ISubscriber.cs new file mode 100644 index 0000000..3ec09c8 --- /dev/null +++ b/PettingZoo.Core/Connection/ISubscriber.cs @@ -0,0 +1,26 @@ +using System; + +namespace PettingZoo.Core.Connection +{ + public interface ISubscriber : IAsyncDisposable + { + string Exchange {get; } + string RoutingKey { get; } + + event EventHandler? MessageReceived; + + void Start(); + } + + + public class MessageReceivedEventArgs : EventArgs + { + public MessageInfo MessageInfo { get; } + + + public MessageReceivedEventArgs(MessageInfo messageInfo) + { + MessageInfo = messageInfo; + } + } +} diff --git a/PettingZoo.Core/Connection/MessageInfo.cs b/PettingZoo.Core/Connection/MessageInfo.cs new file mode 100644 index 0000000..f70abd6 --- /dev/null +++ b/PettingZoo.Core/Connection/MessageInfo.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace PettingZoo.Core.Connection +{ + public class MessageInfo + { + public DateTime Timestamp { get; } + public string Exchange { get; } + public string RoutingKey { get; } + public byte[] Body { get; } + public IDictionary Properties { get; } + + public MessageInfo(string exchange, string routingKey, byte[] body, IDictionary properties, DateTime timestamp) + { + Exchange = exchange; + RoutingKey = routingKey; + Body = body; + Properties = properties; + Timestamp = timestamp; + } + } +} diff --git a/PettingZoo.Core/PettingZoo.Core.csproj b/PettingZoo.Core/PettingZoo.Core.csproj new file mode 100644 index 0000000..951e5aa --- /dev/null +++ b/PettingZoo.Core/PettingZoo.Core.csproj @@ -0,0 +1,12 @@ + + + + net5.0 + enable + + + + + + + diff --git a/Model/MessageBodyRenderer.cs b/PettingZoo.Core/Rendering/MessageBodyRenderer.cs similarity index 73% rename from Model/MessageBodyRenderer.cs rename to PettingZoo.Core/Rendering/MessageBodyRenderer.cs index 1ba2e62..acdc033 100644 --- a/Model/MessageBodyRenderer.cs +++ b/PettingZoo.Core/Rendering/MessageBodyRenderer.cs @@ -3,11 +3,11 @@ using System.Collections.Generic; using System.Text; using Newtonsoft.Json; -namespace PettingZoo.Model +namespace PettingZoo.Core.Rendering { public class MessageBodyRenderer { - public static Dictionary> ContentTypeHandlers = new Dictionary> + public static Dictionary> ContentTypeHandlers = new() { { "application/json", RenderJson } }; @@ -15,13 +15,11 @@ namespace PettingZoo.Model public static string Render(byte[] body, string contentType = "") { - Func handler; - - if (ContentTypeHandlers.TryGetValue(contentType, out handler)) - return handler(body); + return ContentTypeHandlers.TryGetValue(contentType, out var handler) + ? handler(body) + : Encoding.UTF8.GetString(body); // ToDo hex output if required - return Encoding.UTF8.GetString(body); } diff --git a/PettingZoo.RabbitMQ/PettingZoo.RabbitMQ.csproj b/PettingZoo.RabbitMQ/PettingZoo.RabbitMQ.csproj new file mode 100644 index 0000000..38e4451 --- /dev/null +++ b/PettingZoo.RabbitMQ/PettingZoo.RabbitMQ.csproj @@ -0,0 +1,17 @@ + + + + net5.0 + enable + + + + + + + + + + + + diff --git a/PettingZoo.RabbitMQ/RabbitMQClientConnection.cs b/PettingZoo.RabbitMQ/RabbitMQClientConnection.cs new file mode 100644 index 0000000..9682070 --- /dev/null +++ b/PettingZoo.RabbitMQ/RabbitMQClientConnection.cs @@ -0,0 +1,142 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PettingZoo.Core.Connection; +using RabbitMQ.Client; + +namespace PettingZoo.RabbitMQ +{ + public class RabbitMQClientConnection : Core.Connection.IConnection + { + private const int ConnectRetryDelay = 5000; + + private readonly CancellationTokenSource connectionTaskToken = new(); + private readonly Task connectionTask; + private readonly object connectionLock = new(); + private global::RabbitMQ.Client.IConnection? connection; + private IModel? model; + + + public event EventHandler? StatusChanged; + + + public RabbitMQClientConnection(ConnectionParams connectionParams) + { + connectionTask = Task.Factory.StartNew(() => TryConnection(connectionParams, connectionTaskToken.Token), CancellationToken.None); + } + + + public async ValueTask DisposeAsync() + { + connectionTaskToken.Cancel(); + if (!connectionTask.IsCompleted) + await connectionTask; + + lock (connectionLock) + { + if (model != null) + { + model.Dispose(); + model = null; + } + + if (connection != null) + { + connection.Dispose(); + connection = null; + } + } + } + + + public ISubscriber Subscribe(string exchange, string routingKey) + { + lock (connectionLock) + { + var subscriber = new RabbitMQClientSubscriber(model, exchange, routingKey); + if (model != null) + return subscriber; + + + void ConnectSubscriber(object? sender, StatusChangedEventArgs args) + { + if (args.Status != ConnectionStatus.Connected) + return; + + lock (connectionLock) + { + if (model == null) + return; + + subscriber.Connected(model); + } + + StatusChanged -= ConnectSubscriber; + } + + + StatusChanged += ConnectSubscriber; + return subscriber; + } + } + + + public Task Publish(MessageInfo messageInfo) + { + if (model == null) + throw new InvalidOperationException("Not connected"); + + model.BasicPublish(messageInfo.Exchange, messageInfo.RoutingKey, false, + RabbitMQClientPropertiesConverter.Convert(messageInfo.Properties, model.CreateBasicProperties()), + messageInfo.Body); + + return Task.CompletedTask; + } + + + private void TryConnection(ConnectionParams connectionParams, CancellationToken cancellationToken) + { + var factory = new ConnectionFactory + { + HostName = connectionParams.Host, + Port = connectionParams.Port, + VirtualHost = connectionParams.VirtualHost, + UserName = connectionParams.Username, + Password = connectionParams.Password + }; + + var statusContext = $"{connectionParams.Host}:{connectionParams.Port}{connectionParams.VirtualHost}"; + + while (!cancellationToken.IsCancellationRequested) + { + DoStatusChanged(ConnectionStatus.Connecting, statusContext); + try + { + connection = factory.CreateConnection(); + model = connection.CreateModel(); + + DoStatusChanged(ConnectionStatus.Connected, statusContext); + break; + } + catch (Exception e) + { + DoStatusChanged(ConnectionStatus.Error, e.Message); + + try + { + Task.Delay(ConnectRetryDelay, cancellationToken).Wait(cancellationToken); + } + catch (OperationCanceledException) + { + } + } + } + } + + + private void DoStatusChanged(ConnectionStatus status, string? context = null) + { + StatusChanged?.Invoke(this, new StatusChangedEventArgs(status, context)); + } + } +} diff --git a/PettingZoo.RabbitMQ/RabbitMQClientConnectionFactory.cs b/PettingZoo.RabbitMQ/RabbitMQClientConnectionFactory.cs new file mode 100644 index 0000000..4850b3f --- /dev/null +++ b/PettingZoo.RabbitMQ/RabbitMQClientConnectionFactory.cs @@ -0,0 +1,12 @@ +using PettingZoo.Core.Connection; + +namespace PettingZoo.RabbitMQ +{ + public class RabbitMQClientConnectionFactory : IConnectionFactory + { + public IConnection CreateConnection(ConnectionParams connectionParams) + { + return new RabbitMQClientConnection(connectionParams); + } + } +} diff --git a/PettingZoo.RabbitMQ/RabbitMQClientPropertiesConverter.cs b/PettingZoo.RabbitMQ/RabbitMQClientPropertiesConverter.cs new file mode 100644 index 0000000..967413f --- /dev/null +++ b/PettingZoo.RabbitMQ/RabbitMQClientPropertiesConverter.cs @@ -0,0 +1,138 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using RabbitMQ.Client; + +namespace PettingZoo.RabbitMQ +{ + public static class RabbitMQClientPropertiesConverter + { + public static IDictionary Convert(IBasicProperties basicProperties) + { + var properties = new Dictionary(); + + if (basicProperties.IsDeliveryModePresent()) + properties.Add(RabbitMQProperties.DeliveryMode, basicProperties.DeliveryMode.ToString(CultureInfo.InvariantCulture)); + + if (basicProperties.IsContentTypePresent()) + properties.Add(RabbitMQProperties.ContentType, basicProperties.ContentType); + + if (basicProperties.IsContentEncodingPresent()) + properties.Add(RabbitMQProperties.ContentEncoding, basicProperties.ContentEncoding); + + if (basicProperties.IsPriorityPresent()) + properties.Add(RabbitMQProperties.Priority, basicProperties.Priority.ToString(CultureInfo.InvariantCulture)); + + if (basicProperties.IsCorrelationIdPresent()) + properties.Add(RabbitMQProperties.Priority, basicProperties.CorrelationId); + + if (basicProperties.IsReplyToPresent()) + properties.Add(RabbitMQProperties.ReplyTo, basicProperties.ReplyTo); + + if (basicProperties.IsExpirationPresent()) + properties.Add(RabbitMQProperties.Expiration, basicProperties.Expiration); + + if (basicProperties.IsMessageIdPresent()) + properties.Add(RabbitMQProperties.MessageId, basicProperties.MessageId); + + if (basicProperties.IsTimestampPresent()) + properties.Add(RabbitMQProperties.Timestamp, basicProperties.Timestamp.UnixTime.ToString(CultureInfo.InvariantCulture)); + + if (basicProperties.IsTypePresent()) + properties.Add(RabbitMQProperties.Type, basicProperties.Type); + + if (basicProperties.IsUserIdPresent()) + properties.Add(RabbitMQProperties.UserId, basicProperties.UserId); + + if (basicProperties.IsAppIdPresent()) + properties.Add(RabbitMQProperties.UserId, basicProperties.AppId); + + if (basicProperties.IsClusterIdPresent()) + properties.Add(RabbitMQProperties.ClusterId, basicProperties.ClusterId); + + // ReSharper disable once InvertIf + if (basicProperties.Headers != null) + { + foreach (var (key, value) in basicProperties.Headers) + properties.Add(key, Encoding.UTF8.GetString((byte[]) value)); + } + + return properties; + } + + + public static IBasicProperties Convert(IDictionary properties, IBasicProperties targetProperties) + { + foreach (var (key, value) in properties) + { + switch (key) + { + case RabbitMQProperties.DeliveryMode: + if (byte.TryParse(value, out var deliveryMode)) + targetProperties.DeliveryMode = deliveryMode; + + break; + + case RabbitMQProperties.ContentType: + targetProperties.ContentType = value; + break; + + case RabbitMQProperties.ContentEncoding: + targetProperties.ContentEncoding = value; + break; + + case RabbitMQProperties.Priority: + if (byte.TryParse(value, out var priority)) + targetProperties.Priority = priority; + + break; + + case RabbitMQProperties.CorrelationId: + targetProperties.CorrelationId = value; + break; + + case RabbitMQProperties.ReplyTo: + targetProperties.ReplyTo = value; + break; + + case RabbitMQProperties.Expiration: + targetProperties.Expiration = value; + break; + + case RabbitMQProperties.MessageId: + targetProperties.MessageId = value; + break; + + case RabbitMQProperties.Timestamp: + if (long.TryParse(value, out var timestamp)) + targetProperties.Timestamp = new AmqpTimestamp(timestamp); + + break; + + case RabbitMQProperties.Type: + targetProperties.Type = value; + break; + + case RabbitMQProperties.UserId: + targetProperties.UserId = value; + break; + + case RabbitMQProperties.AppId: + targetProperties.AppId = value; + break; + + case RabbitMQProperties.ClusterId: + targetProperties.ClusterId = value; + break; + + default: + targetProperties.Headers ??= new Dictionary(); + targetProperties.Headers.Add(key, Encoding.UTF8.GetBytes(value)); + break; + } + } + + return targetProperties; + } + } +} diff --git a/PettingZoo.RabbitMQ/RabbitMQClientSubscriber.cs b/PettingZoo.RabbitMQ/RabbitMQClientSubscriber.cs new file mode 100644 index 0000000..7f7dd26 --- /dev/null +++ b/PettingZoo.RabbitMQ/RabbitMQClientSubscriber.cs @@ -0,0 +1,79 @@ +using System; +using System.Threading.Tasks; +using PettingZoo.Core.Connection; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace PettingZoo.RabbitMQ +{ + public class RabbitMQClientSubscriber : ISubscriber + { + private IModel? model; + + private string? consumerTag; + private bool started; + + public string Exchange { get; } + public string RoutingKey { get; } + public event EventHandler? MessageReceived; + + + public RabbitMQClientSubscriber(IModel? model, string exchange, string routingKey) + { + this.model = model; + Exchange = exchange; + RoutingKey = routingKey; + } + + + public ValueTask DisposeAsync() + { + if (model != null && consumerTag != null && model.IsOpen) + model.BasicCancelNoWait(consumerTag); + + return default; + } + + + public void Start() + { + started = true; + if (model == null) + return; + + var queueName = model.QueueDeclare().QueueName; + model.QueueBind(queueName, Exchange, RoutingKey); + + var consumer = new EventingBasicConsumer(model); + consumer.Received += ClientReceived; + + consumerTag = model.BasicConsume(queueName, true, consumer); + } + + + public void Connected(IModel newModel) + { + model = newModel; + + if (started) + Start(); + } + + + private void ClientReceived(object? sender, BasicDeliverEventArgs args) + { + MessageReceived?.Invoke(this, new MessageReceivedEventArgs( + new MessageInfo( + args.Exchange, + args.RoutingKey, + args.Body.ToArray(), + RabbitMQClientPropertiesConverter.Convert(args.BasicProperties), + args.BasicProperties.Timestamp.UnixTime > 0 + ? DateTimeOffset.FromUnixTimeSeconds(args.BasicProperties.Timestamp.UnixTime).LocalDateTime + : DateTime.Now + ) + )); + } + + } +} diff --git a/Model/RabbitMQProperties.cs b/PettingZoo.RabbitMQ/RabbitMQProperties.cs similarity index 90% rename from Model/RabbitMQProperties.cs rename to PettingZoo.RabbitMQ/RabbitMQProperties.cs index a8378b5..5cab9d6 100644 --- a/Model/RabbitMQProperties.cs +++ b/PettingZoo.RabbitMQ/RabbitMQProperties.cs @@ -1,6 +1,6 @@ -namespace PettingZoo.Model +namespace PettingZoo.RabbitMQ { - static class RabbitMQProperties + public static class RabbitMQProperties { public const string ContentType = "content-type"; public const string ContentEncoding = "content-encoding"; diff --git a/PettingZoo.RabbitMQ/RabbitMQPropertiesExtensions.cs b/PettingZoo.RabbitMQ/RabbitMQPropertiesExtensions.cs new file mode 100644 index 0000000..41cd78f --- /dev/null +++ b/PettingZoo.RabbitMQ/RabbitMQPropertiesExtensions.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace PettingZoo.RabbitMQ +{ + public static class RabbitMQPropertiesExtensions + { + public static string ContentType(this IDictionary properties) + { + return properties.TryGetValue(RabbitMQProperties.ContentType, out var value) + ? value + : ""; + } + } +} diff --git a/PettingZoo.csproj b/PettingZoo.csproj deleted file mode 100644 index 6b2b336..0000000 --- a/PettingZoo.csproj +++ /dev/null @@ -1,188 +0,0 @@ - - - - - Debug - AnyCPU - {24819D09-C747-4356-B686-D9DE9CAA6F59} - WinExe - Properties - PettingZoo - PettingZoo - v4.5 - 512 - {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - 4 - publish\ - true - Disk - false - Foreground - 7 - Days - false - false - true - 0 - 1.0.0.%2a - false - false - true - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - false - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - PettingZoo.ico - - - - packages\AutoMapper.4.2.1\lib\net45\AutoMapper.dll - True - - - packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll - True - - - packages\RabbitMQ.Client.3.6.2\lib\net45\RabbitMQ.Client.dll - True - - - packages\SimpleInjector.3.1.5\lib\net45\SimpleInjector.dll - True - - - - - - - - - - - 4.0 - - - - - - - - MSBuild:Compile - Designer - - - Designer - MSBuild:Compile - - - Designer - MSBuild:Compile - - - Designer - MSBuild:Compile - - - MSBuild:Compile - Designer - - - App.xaml - Code - - - - - - - - - - - - - - - - - - - - ConnectionWindow.xaml - - - MainWindow.xaml - Code - - - - - Code - - - True - True - Resources.resx - - - PublicResXFileCodeGenerator - Resources.Designer.cs - - - - - - - - - - False - Microsoft .NET Framework 4.5 %28x86 and x64%29 - true - - - False - .NET Framework 3.5 SP1 Client Profile - false - - - False - .NET Framework 3.5 SP1 - false - - - - - - - - \ No newline at end of file diff --git a/PettingZoo.sln b/PettingZoo.sln index 125cd4c..642c3a1 100644 --- a/PettingZoo.sln +++ b/PettingZoo.sln @@ -1,9 +1,18 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.40629.0 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31911.196 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo", "PettingZoo.csproj", "{24819D09-C747-4356-B686-D9DE9CAA6F59}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo", "PettingZoo\PettingZoo.csproj", "{24819D09-C747-4356-B686-D9DE9CAA6F59}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A64E3FB8-7606-4A05-BF10-D83FD0E80D2D}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.Core", "PettingZoo.Core\PettingZoo.Core.csproj", "{AD20CA14-6272-4C50-819D-F9FE6A963DB1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.RabbitMQ", "PettingZoo.RabbitMQ\PettingZoo.RabbitMQ.csproj", "{220149F3-A8D6-44ED-B3B6-DFE506EB018A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,8 +24,19 @@ Global {24819D09-C747-4356-B686-D9DE9CAA6F59}.Debug|Any CPU.Build.0 = Debug|Any CPU {24819D09-C747-4356-B686-D9DE9CAA6F59}.Release|Any CPU.ActiveCfg = Release|Any CPU {24819D09-C747-4356-B686-D9DE9CAA6F59}.Release|Any CPU.Build.0 = Release|Any CPU + {AD20CA14-6272-4C50-819D-F9FE6A963DB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD20CA14-6272-4C50-819D-F9FE6A963DB1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD20CA14-6272-4C50-819D-F9FE6A963DB1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD20CA14-6272-4C50-819D-F9FE6A963DB1}.Release|Any CPU.Build.0 = Release|Any CPU + {220149F3-A8D6-44ED-B3B6-DFE506EB018A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {220149F3-A8D6-44ED-B3B6-DFE506EB018A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {220149F3-A8D6-44ED-B3B6-DFE506EB018A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {220149F3-A8D6-44ED-B3B6-DFE506EB018A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {07CE270E-9E57-49CD-8D5C-79C7B7A98517} + EndGlobalSection EndGlobal diff --git a/App.xaml b/PettingZoo/App.xaml similarity index 79% rename from App.xaml rename to PettingZoo/App.xaml index a9025ab..8fa298e 100644 --- a/App.xaml +++ b/PettingZoo/App.xaml @@ -1,12 +1,12 @@  + ShutdownMode="OnMainWindowClose" + DispatcherUnhandledException="App_OnDispatcherUnhandledException"> - diff --git a/PettingZoo/App.xaml.cs b/PettingZoo/App.xaml.cs new file mode 100644 index 0000000..0a82edf --- /dev/null +++ b/PettingZoo/App.xaml.cs @@ -0,0 +1,13 @@ +using System.Windows; +using System.Windows.Threading; + +namespace PettingZoo +{ + public partial class App + { + private void App_OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) + { + _ = MessageBox.Show($"Unhandled exception: {e.Exception.Message}", "Petting Zoo - Exception", MessageBoxButton.OK, MessageBoxImage.Error); + } + } +} diff --git a/Images/Clear.svg b/PettingZoo/Images/Clear.svg similarity index 100% rename from Images/Clear.svg rename to PettingZoo/Images/Clear.svg diff --git a/Images/Connect.svg b/PettingZoo/Images/Connect.svg similarity index 100% rename from Images/Connect.svg rename to PettingZoo/Images/Connect.svg diff --git a/Images/Disconnect.svg b/PettingZoo/Images/Disconnect.svg similarity index 100% rename from Images/Disconnect.svg rename to PettingZoo/Images/Disconnect.svg diff --git a/Images/PettingZoo-48.png b/PettingZoo/Images/PettingZoo-48.png similarity index 100% rename from Images/PettingZoo-48.png rename to PettingZoo/Images/PettingZoo-48.png diff --git a/Images/PettingZoo.ai b/PettingZoo/Images/PettingZoo.ai similarity index 100% rename from Images/PettingZoo.ai rename to PettingZoo/Images/PettingZoo.ai diff --git a/Images/PettingZoo.ico b/PettingZoo/Images/PettingZoo.ico similarity index 100% rename from Images/PettingZoo.ico rename to PettingZoo/Images/PettingZoo.ico diff --git a/PettingZoo/Images/Publish.svg b/PettingZoo/Images/Publish.svg new file mode 100644 index 0000000..f83e762 --- /dev/null +++ b/PettingZoo/Images/Publish.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PettingZoo/Images/PublishSend.svg b/PettingZoo/Images/PublishSend.svg new file mode 100644 index 0000000..a564250 --- /dev/null +++ b/PettingZoo/Images/PublishSend.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PettingZoo/Images/Subscribe.svg b/PettingZoo/Images/Subscribe.svg new file mode 100644 index 0000000..72ad0cb --- /dev/null +++ b/PettingZoo/Images/Subscribe.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PettingZoo/PettingZoo.csproj b/PettingZoo/PettingZoo.csproj new file mode 100644 index 0000000..20c88b9 --- /dev/null +++ b/PettingZoo/PettingZoo.csproj @@ -0,0 +1,112 @@ + + + + WinExe + net5.0-windows + true + Mark van Renswoude + Petting Zoo + Petting Zoo - a live RabbitMQ message viewer + + enable + true + PettingZoo.Program + + + PettingZoo.ico + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ConnectionWindowStrings.resx + True + True + + + MainWindowStrings.resx + True + True + + + SubscribeWindowStrings.resx + True + True + + + PublisherViewStrings.resx + True + True + + + SubscriberViewStrings.resx + True + True + + + + + + ConnectionWindowStrings.Designer.cs + PublicResXFileCodeGenerator + + + MainWindowStrings.Designer.cs + PublicResXFileCodeGenerator + + + SubscribeWindowStrings.Designer.cs + PublicResXFileCodeGenerator + + + PublisherViewStrings.Designer.cs + PublicResXFileCodeGenerator + + + SubscriberViewStrings.Designer.cs + PublicResXFileCodeGenerator + + + + + + $(DefaultXamlRuntime) + + + $(DefaultXamlRuntime) + + + $(DefaultXamlRuntime) + + + $(DefaultXamlRuntime) + + + $(DefaultXamlRuntime) + + + + diff --git a/PettingZoo.ico b/PettingZoo/PettingZoo.ico similarity index 100% rename from PettingZoo.ico rename to PettingZoo/PettingZoo.ico diff --git a/App.xaml.cs b/PettingZoo/Program.cs similarity index 51% rename from App.xaml.cs rename to PettingZoo/Program.cs index 4089151..60752d2 100644 --- a/App.xaml.cs +++ b/PettingZoo/Program.cs @@ -5,16 +5,22 @@ using System.Reflection; using System.Windows; using System.Windows.Markup; using Newtonsoft.Json; -using PettingZoo.Model; -using PettingZoo.View; -using PettingZoo.ViewModel; +using PettingZoo.Core.Connection; +using PettingZoo.RabbitMQ; +using PettingZoo.Settings; +using PettingZoo.UI.Connection; +using PettingZoo.UI.Main; +using PettingZoo.UI.Subscribe; +using PettingZoo.UI.Tab; +using PettingZoo.UI.Tab.Subscriber; using SimpleInjector; namespace PettingZoo { - public partial class App + public static class Program { - public void ApplicationStartup(object sender, StartupEventArgs e) + [STAThread] + public static void Main() { // WPF defaults to US for date formatting in bindings, this fixes it FrameworkElement.LanguageProperty.OverrideMetadata(typeof(FrameworkElement), new FrameworkPropertyMetadata( @@ -28,28 +34,50 @@ namespace PettingZoo private static Container Bootstrap() { var container = new Container(); + + // See comments in RunApplication + container.Options.EnableAutoVerification = false; container.RegisterSingleton(() => new UserSettings(new AppDataSettingsSerializer("Settings.json"))); container.Register(); - container.Register(); + container.Register(); + container.Register(); + container.Register(); container.Register(); - container.Register(); - - // Note: don't run Verify! It'll create a MainWindow which will then become - // Application.Current.MainWindow and prevent the process from shutting down. - + return container; } - private static void RunApplication(Container container) + private static void RunApplication(Container container) { - var mainWindow = container.GetInstance(); - mainWindow.Closed += (sender, args) => container.Dispose(); + try + { + var app = new App(); + app.InitializeComponent(); - mainWindow.Show(); + #if DEBUG + // Verify container after initialization to prevent issues loading the resource dictionaries + container.Verify(); + + // This causes the MainWindow and Windows properties to be populated however, which we don't want + // because then the app does not close properly when using OnMainWindowClose, so clean up the mess + app.MainWindow = null; + foreach (var window in app.Windows) + ((Window)window).Close(); + + // All this is the reason we only perform verification in debug builds + #endif + + var mainWindow = container.GetInstance(); + _ = app.Run(mainWindow); + } + catch (Exception) + { + // TODO Log the exception and exit + } } @@ -64,8 +92,8 @@ namespace PettingZoo var companyName = GetProductInfo().Company; var productName = GetProductInfo().Product; - path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - companyName, productName); + path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + companyName, productName); fullPath = Path.Combine(path, filename); } @@ -79,7 +107,7 @@ namespace PettingZoo public void Write(UserSettings settings) { - Directory.CreateDirectory(path); + _ = Directory.CreateDirectory(path); File.WriteAllText(fullPath, JsonConvert.SerializeObject(settings, Formatting.Indented)); } @@ -87,11 +115,10 @@ namespace PettingZoo private T GetProductInfo() { var attributes = GetType().Assembly.GetCustomAttributes(typeof(T), true); - if (attributes.Length == 0) - throw new Exception("Missing product information in assembly"); - - return (T)attributes[0]; + return attributes.Length == 0 + ? throw new Exception("Missing product information in assembly") + : (T) attributes[0]; } } } -} +} \ No newline at end of file diff --git a/Model/UserSettings.cs b/PettingZoo/Settings/UserSettings.cs similarity index 91% rename from Model/UserSettings.cs rename to PettingZoo/Settings/UserSettings.cs index cefe00b..0d48a40 100644 --- a/Model/UserSettings.cs +++ b/PettingZoo/Settings/UserSettings.cs @@ -1,4 +1,4 @@ -namespace PettingZoo.Model +namespace PettingZoo.Settings { public interface IUserSettingsSerializer { @@ -15,6 +15,7 @@ public string LastUsername { get; set; } public string LastPassword { get; set; } + //public bool LastSubscribe { get; set; } public string LastExchange { get; set; } public string LastRoutingKey { get; set; } @@ -27,7 +28,7 @@ LastUsername = "guest"; LastPassword = "guest"; - LastExchange = "amqp"; + LastExchange = ""; LastRoutingKey = "#"; } } @@ -35,7 +36,7 @@ public class UserSettings { - public ConnectionWindowSettings ConnectionWindow { get; private set; } + public ConnectionWindowSettings ConnectionWindow { get; } private readonly IUserSettingsSerializer serializer; diff --git a/Style.xaml b/PettingZoo/Style.xaml similarity index 89% rename from Style.xaml rename to PettingZoo/Style.xaml index f5896b0..bc8459b 100644 --- a/Style.xaml +++ b/PettingZoo/Style.xaml @@ -1,6 +1,6 @@  + xmlns:ui="clr-namespace:PettingZoo.UI"> - @@ -59,6 +59,8 @@ - \ No newline at end of file diff --git a/PettingZoo/UI/BaseViewModel.cs b/PettingZoo/UI/BaseViewModel.cs new file mode 100644 index 0000000..33b7909 --- /dev/null +++ b/PettingZoo/UI/BaseViewModel.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace PettingZoo.UI +{ + public class BaseViewModel : INotifyPropertyChanged + { + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void RaisePropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + + protected virtual void RaiseOtherPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + + protected bool SetField(ref T field, T value, IEqualityComparer? comparer = null, [CallerMemberName] string? propertyName = null, + params string[]? otherPropertiesChanged) + { + if ((comparer ?? EqualityComparer.Default).Equals(field, value)) + return false; + + field = value; + RaisePropertyChanged(propertyName); + + if (otherPropertiesChanged == null) + return true; + + foreach (var otherProperty in otherPropertiesChanged) + RaisePropertyChanged(otherProperty); + + return true; + } + } +} \ No newline at end of file diff --git a/PettingZoo/UI/Connection/ConnectionViewModel.cs b/PettingZoo/UI/Connection/ConnectionViewModel.cs new file mode 100644 index 0000000..b1eacb6 --- /dev/null +++ b/PettingZoo/UI/Connection/ConnectionViewModel.cs @@ -0,0 +1,117 @@ +using System; +using System.Windows.Input; + +// TODO validate input + +namespace PettingZoo.UI.Connection +{ + public class ConnectionViewModel : BaseViewModel + { + private string host; + private string virtualHost; + private int port; + private string username; + private string password; + + private bool subscribe; + private string exchange; + private string routingKey; + + + public string Host + { + get => host; + set => SetField(ref host, value); + } + + public string VirtualHost + { + get => virtualHost; + set => SetField(ref virtualHost, value); + } + + public int Port + { + get => port; + set => SetField(ref port, value); + } + + public string Username + { + get => username; + set => SetField(ref username, value); + } + + public string Password + { + get => password; + set => SetField(ref password, value); + } + + + public bool Subscribe + { + get => subscribe; + set => SetField(ref subscribe, value); + } + + public string Exchange + { + get => exchange; + set => SetField(ref exchange, value); + } + + public string RoutingKey + { + get => routingKey; + set => SetField(ref routingKey, value); + } + + + public ICommand OkCommand { get; } + + public event EventHandler? OkClick; + + + public ConnectionViewModel(ConnectionDialogParams model) + { + OkCommand = new DelegateCommand(OkExecute, OkCanExecute); + + host = model.Host; + virtualHost = model.VirtualHost; + port = model.Port; + username = model.Username; + password = model.Password; + + subscribe = model.Subscribe; + exchange = model.Exchange; + routingKey = model.RoutingKey; + } + + + public ConnectionDialogParams ToModel() + { + return new(Host, VirtualHost, Port, Username, Password, Subscribe, Exchange, RoutingKey); + } + + + private void OkExecute() + { + OkClick?.Invoke(this, EventArgs.Empty); + } + + + private static bool OkCanExecute() + { + return true; + } + } + + + public class DesignTimeConnectionViewModel : ConnectionViewModel + { + public DesignTimeConnectionViewModel() : base(ConnectionDialogParams.Default) + { + } + } +} diff --git a/View/ConnectionWindow.xaml b/PettingZoo/UI/Connection/ConnectionWindow.xaml similarity index 57% rename from View/ConnectionWindow.xaml rename to PettingZoo/UI/Connection/ConnectionWindow.xaml index 700f928..0c5afd8 100644 --- a/View/ConnectionWindow.xaml +++ b/PettingZoo/UI/Connection/ConnectionWindow.xaml @@ -1,27 +1,26 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PettingZoo/UI/Main/MainWindow.xaml.cs b/PettingZoo/UI/Main/MainWindow.xaml.cs new file mode 100644 index 0000000..c41d85e --- /dev/null +++ b/PettingZoo/UI/Main/MainWindow.xaml.cs @@ -0,0 +1,50 @@ +using System; +using System.Windows; +using PettingZoo.Core.Connection; +using PettingZoo.UI.Connection; +using PettingZoo.UI.Subscribe; +using PettingZoo.UI.Tab; + +namespace PettingZoo.UI.Main +{ + // TODO support undocking tabs (and redocking afterwards) + // TODO allow tab reordering + + #pragma warning disable CA1001 // MainWindow can't be IDisposable, handled instead in OnDispatcherShutDownStarted + public partial class MainWindow + { + private readonly MainWindowViewModel viewModel; + + + public MainWindow(IConnectionFactory connectionFactory, IConnectionDialog connectionDialog, ISubscribeDialog subscribeDialog, ITabFactory tabFactory) + { + WindowStartupLocation = WindowStartupLocation.CenterScreen; + + InitializeComponent(); + viewModel = new MainWindowViewModel(connectionFactory, connectionDialog, subscribeDialog, tabFactory); + DataContext = viewModel; + + Dispatcher.ShutdownStarted += OnDispatcherShutDownStarted; + } + + + private async void OnDispatcherShutDownStarted(object? sender, EventArgs e) + { + if (DataContext is IAsyncDisposable disposable) + await disposable.DisposeAsync(); + } + + + private void MainWindow_OnLoaded(object sender, RoutedEventArgs e) + { + viewModel.ConnectCommand.Execute(null); + } + + + private void MainWindow_OnClosed(object? sender, EventArgs e) + { + var _ = Application.Current.Windows; + } + } + #pragma warning restore CA1001 +} diff --git a/Properties/Resources.Designer.cs b/PettingZoo/UI/Main/MainWindowStrings.Designer.cs similarity index 55% rename from Properties/Resources.Designer.cs rename to PettingZoo/UI/Main/MainWindowStrings.Designer.cs index 4b0bc29..8b3f52f 100644 --- a/Properties/Resources.Designer.cs +++ b/PettingZoo/UI/Main/MainWindowStrings.Designer.cs @@ -8,7 +8,10 @@ // //------------------------------------------------------------------------------ -namespace PettingZoo.Properties { +namespace PettingZoo.UI.Main { + using System; + + /// /// A strongly-typed resource class, for looking up localized strings, etc. /// @@ -16,17 +19,17 @@ namespace PettingZoo.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - public class Resources { + public class MainWindowStrings { private static global::System.Resources.ResourceManager resourceMan; private static global::System.Globalization.CultureInfo resourceCulture; [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { + internal MainWindowStrings() { } /// @@ -36,7 +39,7 @@ namespace PettingZoo.Properties { public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.Properties.Resources", typeof(Resources).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.UI.Main.MainWindowStrings", typeof(MainWindowStrings).Assembly); resourceMan = temp; } return resourceMan; @@ -58,92 +61,47 @@ namespace PettingZoo.Properties { } /// - /// Looks up a localized string similar to Cancel. + /// Looks up a localized string similar to Connect. /// - public static string ButtonCancel { + public static string CommandConnect { get { - return ResourceManager.GetString("ButtonCancel", resourceCulture); + return ResourceManager.GetString("CommandConnect", resourceCulture); } } /// - /// Looks up a localized string similar to OK. + /// Looks up a localized string similar to Disconnect. /// - public static string ButtonOK { + public static string CommandDisconnect { get { - return ResourceManager.GetString("ButtonOK", resourceCulture); + return ResourceManager.GetString("CommandDisconnect", resourceCulture); } } /// - /// Looks up a localized string similar to Exchange:. + /// Looks up a localized string similar to New Publisher. /// - public static string ConnectionExchange { + public static string CommandPublish { get { - return ResourceManager.GetString("ConnectionExchange", resourceCulture); + return ResourceManager.GetString("CommandPublish", resourceCulture); } } /// - /// Looks up a localized string similar to Host:. + /// Looks up a localized string similar to New Subscriber.... /// - public static string ConnectionHost { + public static string CommandSubscribe { get { - return ResourceManager.GetString("ConnectionHost", resourceCulture); + return ResourceManager.GetString("CommandSubscribe", resourceCulture); } } /// - /// Looks up a localized string similar to Password:. + /// Looks up a localized string similar to Close tab. /// - public static string ConnectionPassword { + public static string ContextMenuCloseTab { get { - return ResourceManager.GetString("ConnectionPassword", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Port:. - /// - public static string ConnectionPort { - get { - return ResourceManager.GetString("ConnectionPort", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Routing key:. - /// - public static string ConnectionRoutingKey { - get { - return ResourceManager.GetString("ConnectionRoutingKey", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Username:. - /// - public static string ConnectionUsername { - get { - return ResourceManager.GetString("ConnectionUsername", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Virtual host:. - /// - public static string ConnectionVirtualHost { - get { - return ResourceManager.GetString("ConnectionVirtualHost", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Connection parameters. - /// - public static string ConnectionWindowTitle { - get { - return ResourceManager.GetString("ConnectionWindowTitle", resourceCulture); + return ResourceManager.GetString("ContextMenuCloseTab", resourceCulture); } } @@ -165,51 +123,6 @@ namespace PettingZoo.Properties { } } - /// - /// Looks up a localized string similar to Petting Zoo - a RabbitMQ live message viewer. - /// - public static string MainWindowTitle { - get { - return ResourceManager.GetString("MainWindowTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Body. - /// - public static string PanelTitleBody { - get { - return ResourceManager.GetString("PanelTitleBody", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Properties. - /// - public static string PanelTitleProperties { - get { - return ResourceManager.GetString("PanelTitleProperties", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Name. - /// - public static string PropertyName { - get { - return ResourceManager.GetString("PropertyName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value. - /// - public static string PropertyValue { - get { - return ResourceManager.GetString("PropertyValue", resourceCulture); - } - } - /// /// Looks up a localized string similar to Connected. /// @@ -245,5 +158,14 @@ namespace PettingZoo.Properties { return ResourceManager.GetString("StatusError", resourceCulture); } } + + /// + /// Looks up a localized string similar to Petting Zoo - a RabbitMQ live message viewer. + /// + public static string WindowTitle { + get { + return ResourceManager.GetString("WindowTitle", resourceCulture); + } + } } } diff --git a/Properties/Resources.resx b/PettingZoo/UI/Main/MainWindowStrings.resx similarity index 83% rename from Properties/Resources.resx rename to PettingZoo/UI/Main/MainWindowStrings.resx index e9c49e4..6df2e25 100644 --- a/Properties/Resources.resx +++ b/PettingZoo/UI/Main/MainWindowStrings.resx @@ -117,35 +117,20 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Cancel + + Connect - - OK + + Disconnect - - Exchange: + + New Publisher - - Host: + + New Subscriber... - - Password: - - - Port: - - - Routing key: - - - Username: - - - Virtual host: - - - Connection parameters + + Close tab Non-persistent @@ -153,21 +138,6 @@ Persistent - - Petting Zoo - a RabbitMQ live message viewer - - - Body - - - Properties - - - Name - - - Value - Connected @@ -180,4 +150,7 @@ Error: {0} + + Petting Zoo - a RabbitMQ live message viewer + \ No newline at end of file diff --git a/PettingZoo/UI/Main/MainWindowViewModel.cs b/PettingZoo/UI/Main/MainWindowViewModel.cs new file mode 100644 index 0000000..a1b594e --- /dev/null +++ b/PettingZoo/UI/Main/MainWindowViewModel.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; +using PettingZoo.Core.Connection; +using PettingZoo.UI.Connection; +using PettingZoo.UI.Subscribe; +using PettingZoo.UI.Tab; + +namespace PettingZoo.UI.Main +{ + public class MainWindowViewModel : BaseViewModel, IAsyncDisposable + { + private readonly IConnectionFactory connectionFactory; + private readonly IConnectionDialog connectionDialog; + private readonly ISubscribeDialog subscribeDialog; + private readonly ITabFactory tabFactory; + + private ConnectionDialogParams? connectionDialogParams; + private SubscribeDialogParams? subscribeDialogParams; + private IConnection? connection; + private string connectionStatus; + private ITab? activeTab; + + private readonly DelegateCommand connectCommand; + private readonly DelegateCommand disconnectCommand; + private readonly DelegateCommand publishCommand; + private readonly DelegateCommand subscribeCommand; + private readonly DelegateCommand closeTabCommand; + + + public string ConnectionStatus + { + get => connectionStatus; + private set => SetField(ref connectionStatus, value); + } + + + public ObservableCollection Tabs { get; } + + public ITab? ActiveTab + { + get => activeTab; + set => SetField(ref activeTab, value, otherPropertiesChanged: new [] + { + nameof(ToolbarCommands), + nameof(ToolbarCommandsSeparatorVisibility) + }); + } + + public ICommand ConnectCommand => connectCommand; + public ICommand DisconnectCommand => disconnectCommand; + public ICommand PublishCommand => publishCommand; + public ICommand SubscribeCommand => subscribeCommand; + public ICommand CloseTabCommand => closeTabCommand; + + public IEnumerable ToolbarCommands => ActiveTab is ITabToolbarCommands tabToolbarCommands + ? tabToolbarCommands.ToolbarCommands + : Enumerable.Empty(); + + public Visibility ToolbarCommandsSeparatorVisibility => + ToolbarCommands.Any() ? Visibility.Visible : Visibility.Collapsed; + + + public MainWindowViewModel(IConnectionFactory connectionFactory, IConnectionDialog connectionDialog, + ISubscribeDialog subscribeDialog, ITabFactory tabFactory) + { + this.connectionFactory = connectionFactory; + this.connectionDialog = connectionDialog; + this.subscribeDialog = subscribeDialog; + this.tabFactory = tabFactory; + + connectionStatus = GetConnectionStatus(null); + + Tabs = new ObservableCollection(); + connectCommand = new DelegateCommand(ConnectExecute); + disconnectCommand = new DelegateCommand(DisconnectExecute, IsConnectedCanExecute); + publishCommand = new DelegateCommand(PublishExecute, IsConnectedCanExecute); + subscribeCommand = new DelegateCommand(SubscribeExecute, IsConnectedCanExecute); + closeTabCommand = new DelegateCommand(CloseTabExecute, CloseTabCanExecute); + } + + + public async ValueTask DisposeAsync() + { + if (connection != null) + await connection.DisposeAsync(); + } + + + private async void ConnectExecute() + { + //var newParams = connectionDialog.Show(connectionDialogParams); + var newParams = new ConnectionDialogParams("localhost", "/", 5672, "guest", "guest", true, "lef", "#"); + if (newParams == null) + return; + + if (connection != null) + await connection.DisposeAsync(); + + connectionDialogParams = newParams; + connection = connectionFactory.CreateConnection(new ConnectionParams( + connectionDialogParams.Host, connectionDialogParams.VirtualHost, connectionDialogParams.Port, + connectionDialogParams.Username, connectionDialogParams.Password)); + connection.StatusChanged += ConnectionStatusChanged; + + if (connectionDialogParams.Subscribe) + { + var subscriber = connection.Subscribe(connectionDialogParams.Exchange, connectionDialogParams.RoutingKey); + AddTab(tabFactory.CreateSubscriberTab(CloseTabCommand, subscriber)); + + } + + ConnectionChanged(); + } + + + private async void DisconnectExecute() + { + Tabs.Clear(); + + if (connection != null) + { + await connection.DisposeAsync(); + connection = null; + } + + connectionDialogParams = null; + ConnectionStatus = GetConnectionStatus(null); + + ConnectionChanged(); + } + + + private void SubscribeExecute() + { + if (connection == null) + return; + + var newParams = subscribeDialog.Show(subscribeDialogParams); + if (newParams == null) + return; + + subscribeDialogParams = newParams; + + var subscriber = connection.Subscribe(subscribeDialogParams.Exchange, subscribeDialogParams.RoutingKey); + AddTab(tabFactory.CreateSubscriberTab(CloseTabCommand, subscriber)); + } + + + private void PublishExecute() + { + if (connection == null) + return; + + AddTab(tabFactory.CreatePublisherTab(CloseTabCommand, connection)); + } + + + private bool IsConnectedCanExecute() + { + return connection != null; + } + + + private void CloseTabExecute() + { + if (ActiveTab == null) + return; + + var activeTabIndex = Tabs.IndexOf(ActiveTab); + if (activeTabIndex == -1) + return; + + Tabs.RemoveAt(activeTabIndex); + + if (activeTabIndex == Tabs.Count) + activeTabIndex--; + + ActiveTab = activeTabIndex >= 0 ? Tabs[activeTabIndex] : null; + closeTabCommand.RaiseCanExecuteChanged(); + } + + + private bool CloseTabCanExecute() + { + return ActiveTab != null; + } + + + private void AddTab(ITab tab) + { + Tabs.Add(tab); + ActiveTab = tab; + + closeTabCommand.RaiseCanExecuteChanged(); + } + + + private void ConnectionChanged() + { + disconnectCommand.RaiseCanExecuteChanged(); + subscribeCommand.RaiseCanExecuteChanged(); + publishCommand.RaiseCanExecuteChanged(); + } + + private void ConnectionStatusChanged(object? sender, StatusChangedEventArgs args) + { + ConnectionStatus = GetConnectionStatus(args); + } + + + + private static string GetConnectionStatus(StatusChangedEventArgs? args) + { + return args?.Status switch + { + Core.Connection.ConnectionStatus.Connecting => string.Format(MainWindowStrings.StatusConnecting, args.Context), + Core.Connection.ConnectionStatus.Connected => string.Format(MainWindowStrings.StatusConnected, args.Context), + Core.Connection.ConnectionStatus.Error => string.Format(MainWindowStrings.StatusError, args.Context), + Core.Connection.ConnectionStatus.Disconnected => MainWindowStrings.StatusDisconnected, + _ => MainWindowStrings.StatusDisconnected + }; + } + } + + + public class DesignTimeMainWindowViewModel : MainWindowViewModel + { + public DesignTimeMainWindowViewModel() : base(null!, null!, null!, null!) + { + } + } +} \ No newline at end of file diff --git a/Infrastructure/PasswordBoxAssistant.cs b/PettingZoo/UI/PasswordBoxAssistant.cs similarity index 92% rename from Infrastructure/PasswordBoxAssistant.cs rename to PettingZoo/UI/PasswordBoxAssistant.cs index 3ca477d..b27587b 100644 --- a/Infrastructure/PasswordBoxAssistant.cs +++ b/PettingZoo/UI/PasswordBoxAssistant.cs @@ -1,7 +1,7 @@ using System.Windows; using System.Windows.Controls; -namespace PettingZoo.Infrastructure +namespace PettingZoo.UI { // Source: http://blog.functionalfun.net/2008/06/wpf-passwordbox-and-data-binding.html public static class PasswordBoxAssistant @@ -35,9 +35,7 @@ namespace PettingZoo.Infrastructure var newPassword = (string) e.NewValue; if (!GetUpdatingPassword(box)) - { box.Password = newPassword; - } box.PasswordChanged += HandlePasswordChanged; } @@ -46,26 +44,19 @@ namespace PettingZoo.Infrastructure { // when the BindPassword attached property is set on a PasswordBox, // start listening to its PasswordChanged event - - var box = dp as PasswordBox; - - if (box == null) + if (dp is not PasswordBox box) { return; } - var wasBound = (bool) (e.OldValue); - var needToBind = (bool) (e.NewValue); + var wasBound = (bool)e.OldValue; + var needToBind = (bool)e.NewValue; if (wasBound) - { box.PasswordChanged -= HandlePasswordChanged; - } if (needToBind) - { box.PasswordChanged += HandlePasswordChanged; - } } private static void HandlePasswordChanged(object sender, RoutedEventArgs e) diff --git a/PettingZoo/UI/Subscribe/ISubscribeDialog.cs b/PettingZoo/UI/Subscribe/ISubscribeDialog.cs new file mode 100644 index 0000000..19c65e2 --- /dev/null +++ b/PettingZoo/UI/Subscribe/ISubscribeDialog.cs @@ -0,0 +1,26 @@ +using System; + +namespace PettingZoo.UI.Subscribe +{ + public interface ISubscribeDialog + { + SubscribeDialogParams? Show(SubscribeDialogParams? defaultParams = null); + } + + + public class SubscribeDialogParams + { + public string Exchange { get; } + public string RoutingKey { get; } + + + public static SubscribeDialogParams Default { get; } = new("", "#"); + + + public SubscribeDialogParams(string exchange, string routingKey) + { + Exchange = exchange; + RoutingKey = routingKey; + } + } +} diff --git a/PettingZoo/UI/Subscribe/SubscribeViewModel.cs b/PettingZoo/UI/Subscribe/SubscribeViewModel.cs new file mode 100644 index 0000000..8750367 --- /dev/null +++ b/PettingZoo/UI/Subscribe/SubscribeViewModel.cs @@ -0,0 +1,66 @@ +using System; +using System.Windows.Input; + +// TODO validate input + +namespace PettingZoo.UI.Subscribe +{ + public class SubscribeViewModel : BaseViewModel + { + private string exchange; + private string routingKey; + + + public string Exchange + { + get => exchange; + set => SetField(ref exchange, value); + } + + public string RoutingKey + { + get => routingKey; + set => SetField(ref routingKey, value); + } + + + public ICommand OkCommand { get; } + + public event EventHandler? OkClick; + + + public SubscribeViewModel(SubscribeDialogParams subscribeParams) + { + OkCommand = new DelegateCommand(OkExecute, OkCanExecute); + + exchange = subscribeParams.Exchange; + routingKey = subscribeParams.RoutingKey; + } + + + public SubscribeDialogParams ToModel() + { + return new(Exchange, RoutingKey); + } + + + private void OkExecute() + { + OkClick?.Invoke(this, EventArgs.Empty); + } + + + private static bool OkCanExecute() + { + return true; + } + } + + + public class DesignTimeSubscribeViewModel : SubscribeViewModel + { + public DesignTimeSubscribeViewModel() : base(SubscribeDialogParams.Default) + { + } + } +} diff --git a/PettingZoo/UI/Subscribe/SubscribeWindow.xaml b/PettingZoo/UI/Subscribe/SubscribeWindow.xaml new file mode 100644 index 0000000..7d8010a --- /dev/null +++ b/PettingZoo/UI/Subscribe/SubscribeWindow.xaml @@ -0,0 +1,41 @@ + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/View/MainWindow.xaml.cs b/View/MainWindow.xaml.cs deleted file mode 100644 index 9cce7cd..0000000 --- a/View/MainWindow.xaml.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Windows; -using PettingZoo.ViewModel; - -namespace PettingZoo.View -{ - public partial class MainWindow - { - public MainWindow(MainViewModel viewModel) - { - WindowStartupLocation = WindowStartupLocation.CenterScreen; - - InitializeComponent(); - DataContext = viewModel; - - Dispatcher.ShutdownStarted += OnDispatcherShutDownStarted; - } - - - private void OnDispatcherShutDownStarted(object sender, EventArgs e) - { - var disposable = DataContext as IDisposable; - if (!ReferenceEquals(null, disposable)) - disposable.Dispose(); - } - } -} diff --git a/ViewModel/ConnectionViewModel.cs b/ViewModel/ConnectionViewModel.cs deleted file mode 100644 index 367eb50..0000000 --- a/ViewModel/ConnectionViewModel.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Windows.Input; -using AutoMapper; -using PettingZoo.Infrastructure; -using PettingZoo.Model; - -namespace PettingZoo.ViewModel -{ - public class ConnectionViewModel : BaseViewModel - { - private static readonly IMapper ModelMapper = new MapperConfiguration(cfg => - cfg.CreateMap().ReverseMap() - ).CreateMapper(); - - - private readonly DelegateCommand okCommand; - - - public string Host { get; set; } - public string VirtualHost { get; set; } - public int Port { get; set; } - public string Username { get; set; } - public string Password { get; set; } - - public string Exchange { get; set; } - public string RoutingKey { get; set; } - - - public ICommand OkCommand { get { return okCommand; } } - - public event EventHandler CloseWindow; - - - public ConnectionViewModel() - { - okCommand = new DelegateCommand(OkExecute, OkCanExecute); - } - - - public ConnectionViewModel(ConnectionInfo model) : this() - { - ModelMapper.Map(model, this); - } - - - public ConnectionInfo ToModel() - { - return ModelMapper.Map(this); - } - - - private void OkExecute() - { - if (CloseWindow != null) - CloseWindow(this, EventArgs.Empty); - } - - - private bool OkCanExecute() - { - return true; - } - } -} diff --git a/ViewModel/MainViewModel.cs b/ViewModel/MainViewModel.cs deleted file mode 100644 index e392b36..0000000 --- a/ViewModel/MainViewModel.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Threading; -using System.Threading.Tasks; -using System.Windows.Input; -using PettingZoo.Infrastructure; -using PettingZoo.Model; -using PettingZoo.Properties; - -namespace PettingZoo.ViewModel -{ - public class MainViewModel : BaseViewModel, IDisposable - { - private readonly TaskScheduler uiScheduler; - private readonly IConnectionInfoBuilder connectionInfoBuilder; - private readonly IConnectionFactory connectionFactory; - - private ConnectionInfo connectionInfo; - private IConnection connection; - private string connectionStatus; - private readonly ObservableCollection messages; - private MessageInfo selectedMessage; - - private readonly DelegateCommand connectCommand; - private readonly DelegateCommand disconnectCommand; - private readonly DelegateCommand clearCommand; - - - public ConnectionInfo ConnectionInfo { - get { return connectionInfo; } - private set - { - if (value == connectionInfo) - return; - - connectionInfo = value; - RaisePropertyChanged(); - } - } - - public string ConnectionStatus - { - get { return connectionStatus; } - private set - { - if (value == connectionStatus) - return; - - connectionStatus = value; - RaisePropertyChanged(); - } - } - - public ObservableCollection Messages { get { return messages; } } - - public MessageInfo SelectedMessage - { - get { return selectedMessage; } - set - { - if (value == selectedMessage) - return; - - selectedMessage = value; - RaisePropertyChanged(); - RaiseOtherPropertyChanged("SelectedMessageBody"); - RaiseOtherPropertyChanged("SelectedMessageProperties"); - } - } - - public string SelectedMessageBody - { - get - { - return SelectedMessage != null - ? MessageBodyRenderer.Render(SelectedMessage.Body, SelectedMessage.ContentType) - : ""; - } - } - - public Dictionary SelectedMessageProperties - { - get { return SelectedMessage != null ? SelectedMessage.Properties : null; } - } - - public ICommand ConnectCommand { get { return connectCommand; } } - public ICommand DisconnectCommand { get { return disconnectCommand; } } - public ICommand ClearCommand { get { return clearCommand; } } - - - public MainViewModel(IConnectionInfoBuilder connectionInfoBuilder, IConnectionFactory connectionFactory) - { - uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(); - - this.connectionInfoBuilder = connectionInfoBuilder; - this.connectionFactory = connectionFactory; - - connectionStatus = GetConnectionStatus(null); - messages = new ObservableCollection(); - - connectCommand = new DelegateCommand(ConnectExecute); - disconnectCommand = new DelegateCommand(DisconnectExecute, DisconnectCanExecute); - clearCommand = new DelegateCommand(ClearExecute, ClearCanExecute); - } - - - public void Dispose() - { - if (connection != null) - { - connection.Dispose(); - connection = null; - } - } - - - private void ConnectExecute() - { - var newInfo = connectionInfoBuilder.Build(); - if (newInfo == null) - return; - - if (connection != null) - connection.Dispose(); - - ConnectionInfo = newInfo; - connection = connectionFactory.CreateConnection(connectionInfo); - connection.MessageReceived += ConnectionMessageReceived; - connection.StatusChanged += ConnectionStatusChanged; - - disconnectCommand.RaiseCanExecuteChanged(); - } - - - private void DisconnectExecute() - { - if (connection != null) - { - connection.Dispose(); - connection = null; - } - - ConnectionInfo = null; - ConnectionStatus = GetConnectionStatus(null); - - disconnectCommand.RaiseCanExecuteChanged(); - } - - - private bool DisconnectCanExecute() - { - return connection != null; - } - - - private void ClearExecute() - { - messages.Clear(); - clearCommand.RaiseCanExecuteChanged(); - } - - - private bool ClearCanExecute() - { - return messages.Count > 0; - } - - - private void ConnectionStatusChanged(object sender, StatusChangedEventArgs args) - { - ConnectionStatus = GetConnectionStatus(args); - } - - - private void ConnectionMessageReceived(object sender, MessageReceivedEventArgs args) - { - RunFromUiScheduler(() => - { - messages.Add(args.MessageInfo); - clearCommand.RaiseCanExecuteChanged(); - }); - } - - - private string GetConnectionStatus(StatusChangedEventArgs args) - { - if (args != null) - switch (args.Status) - { - case Model.ConnectionStatus.Connecting: - return String.Format(Resources.StatusConnecting, args.Context); - - case Model.ConnectionStatus.Connected: - return String.Format(Resources.StatusConnected, args.Context); - - case Model.ConnectionStatus.Error: - return String.Format(Resources.StatusError, args.Context); - } - - return Resources.StatusDisconnected; - } - - - private void RunFromUiScheduler(Action action) - { - Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.None, uiScheduler); - } - } -} \ No newline at end of file diff --git a/packages.config b/packages.config deleted file mode 100644 index 7458215..0000000 --- a/packages.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file