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/App.xaml.cs b/App.xaml.cs deleted file mode 100644 index 4089151..0000000 --- a/App.xaml.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Reflection; -using System.Windows; -using System.Windows.Markup; -using Newtonsoft.Json; -using PettingZoo.Model; -using PettingZoo.View; -using PettingZoo.ViewModel; -using SimpleInjector; - -namespace PettingZoo -{ - public partial class App - { - public void ApplicationStartup(object sender, StartupEventArgs e) - { - // WPF defaults to US for date formatting in bindings, this fixes it - FrameworkElement.LanguageProperty.OverrideMetadata(typeof(FrameworkElement), new FrameworkPropertyMetadata( - XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag))); - - var container = Bootstrap(); - RunApplication(container); - } - - - private static Container Bootstrap() - { - var container = new Container(); - - container.RegisterSingleton(() => new UserSettings(new AppDataSettingsSerializer("Settings.json"))); - - 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) - { - var mainWindow = container.GetInstance(); - mainWindow.Closed += (sender, args) => container.Dispose(); - - mainWindow.Show(); - } - - - private class AppDataSettingsSerializer : IUserSettingsSerializer - { - private readonly string path; - private readonly string fullPath; - - - public AppDataSettingsSerializer(string filename) - { - var companyName = GetProductInfo().Company; - var productName = GetProductInfo().Product; - - path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - companyName, productName); - fullPath = Path.Combine(path, filename); - } - - - public void Read(UserSettings settings) - { - if (File.Exists(fullPath)) - JsonConvert.PopulateObject(File.ReadAllText(fullPath), settings); - } - - - public void Write(UserSettings settings) - { - Directory.CreateDirectory(path); - File.WriteAllText(fullPath, JsonConvert.SerializeObject(settings, Formatting.Indented)); - } - - - 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]; - } - } - } -} diff --git a/GitVersion.yml b/GitVersion.yml new file mode 100644 index 0000000..7989fe4 --- /dev/null +++ b/GitVersion.yml @@ -0,0 +1,6 @@ +mode: ContinuousDeployment +branches: + master: + mode: ContinuousDelivery +ignore: + sha: [] 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/Model/RabbitMQProperties.cs b/Model/RabbitMQProperties.cs deleted file mode 100644 index a8378b5..0000000 --- a/Model/RabbitMQProperties.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace PettingZoo.Model -{ - static class RabbitMQProperties - { - public const string ContentType = "content-type"; - public const string ContentEncoding = "content-encoding"; - public const string DeliveryMode = "delivery-mode"; - public const string Priority = "priority"; - public const string CorrelationId = "correlation-id"; - public const string ReplyTo = "reply-to"; - public const string Expiration = "expiration"; - public const string MessageId = "message-id"; - public const string Timestamp = "timestamp"; - public const string Type = "type"; - public const string UserId = "user-id"; - public const string AppId = "app-id"; - public const string ClusterId = "cluster-id"; - } -} diff --git a/Model/UserSettings.cs b/Model/UserSettings.cs deleted file mode 100644 index cefe00b..0000000 --- a/Model/UserSettings.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace PettingZoo.Model -{ - public interface IUserSettingsSerializer - { - void Read(UserSettings settings); - void Write(UserSettings settings); - } - - - public class ConnectionWindowSettings - { - public string LastHost { get; set; } - public string LastVirtualHost { get; set; } - public int LastPort { get; set; } - public string LastUsername { get; set; } - public string LastPassword { get; set; } - - public string LastExchange { get; set; } - public string LastRoutingKey { get; set; } - - - public ConnectionWindowSettings() - { - LastHost = "localhost"; - LastPort = 5672; - LastVirtualHost = "/"; - LastUsername = "guest"; - LastPassword = "guest"; - - LastExchange = "amqp"; - LastRoutingKey = "#"; - } - } - - - public class UserSettings - { - public ConnectionWindowSettings ConnectionWindow { get; private set; } - - - private readonly IUserSettingsSerializer serializer; - - - public UserSettings(IUserSettingsSerializer serializer) - { - ConnectionWindow = new ConnectionWindowSettings(); - - this.serializer = serializer; - serializer.Read(this); - } - - - public void Save() - { - serializer.Write(this); - } - } -} 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..f4ac798 --- /dev/null +++ b/PettingZoo.Core/Connection/IConnection.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; + +namespace PettingZoo.Core.Connection +{ + public interface IConnection : IAsyncDisposable + { + event EventHandler StatusChanged; + + ISubscriber Subscribe(string exchange, string routingKey); + ISubscriber Subscribe(); + + Task Publish(PublishMessageInfo 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..9beb336 --- /dev/null +++ b/PettingZoo.Core/Connection/ISubscriber.cs @@ -0,0 +1,27 @@ +using System; + +namespace PettingZoo.Core.Connection +{ + public interface ISubscriber : IAsyncDisposable + { + string? QueueName { get; } + string? Exchange {get; } + string? RoutingKey { get; } + + event EventHandler? MessageReceived; + + void Start(); + } + + + public class MessageReceivedEventArgs : EventArgs + { + public ReceivedMessageInfo MessageInfo { get; } + + + public MessageReceivedEventArgs(ReceivedMessageInfo messageInfo) + { + MessageInfo = messageInfo; + } + } +} diff --git a/PettingZoo.Core/Connection/MessageInfo.cs b/PettingZoo.Core/Connection/MessageInfo.cs new file mode 100644 index 0000000..fdc6e86 --- /dev/null +++ b/PettingZoo.Core/Connection/MessageInfo.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; + +namespace PettingZoo.Core.Connection +{ + public class BaseMessageInfo + { + public string Exchange { get; } + public string RoutingKey { get; } + public byte[] Body { get; } + public MessageProperties Properties { get; } + + public BaseMessageInfo(string exchange, string routingKey, byte[] body, MessageProperties properties) + { + Exchange = exchange; + RoutingKey = routingKey; + Body = body; + Properties = properties; + } + } + + + public class ReceivedMessageInfo : BaseMessageInfo + { + public DateTime ReceivedTimestamp { get; } + + public ReceivedMessageInfo(string exchange, string routingKey, byte[] body, MessageProperties properties, DateTime receivedTimestamp) + : base(exchange, routingKey, body, properties) + { + ReceivedTimestamp = receivedTimestamp; + } + } + + + public class PublishMessageInfo : BaseMessageInfo + { + public PublishMessageInfo(string exchange, string routingKey, byte[] body, MessageProperties properties) + : base(exchange, routingKey, body, properties) + { + } + } + + + public enum MessageDeliveryMode + { + NonPersistent = 1, + Persistent = 2 + } + + + public class MessageProperties + { + private static readonly IReadOnlyDictionary EmptyHeaders = new Dictionary(); + + public MessageProperties(IReadOnlyDictionary? headers) + { + Headers = headers ?? EmptyHeaders; + } + + public string? AppId { get; init; } + public string? ContentEncoding { get; init; } + public string? ContentType { get; init; } + public string? CorrelationId { get; init; } + public MessageDeliveryMode? DeliveryMode { get; init; } + public string? Expiration { get; init; } + public IReadOnlyDictionary Headers { get; } + public string? MessageId { get; init; } + public byte? Priority { get; init; } + public string? ReplyTo { get; init; } + public DateTime? Timestamp { get; init; } + public string? Type { get; init; } + public string? UserId { get; init; } + } +} diff --git a/PettingZoo.Core/Generator/IExampleGenerator.cs b/PettingZoo.Core/Generator/IExampleGenerator.cs new file mode 100644 index 0000000..ae49c7a --- /dev/null +++ b/PettingZoo.Core/Generator/IExampleGenerator.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +namespace PettingZoo.Core.Generator +{ + public interface IExampleSource : IDisposable + { + IExampleFolder GetRootFolder(); + } + + + public interface IExampleFolder + { + public string Name { get; } + + public IReadOnlyList Folders { get; } + public IReadOnlyList Messages { get; } + } + + + public interface IExampleMessage + { + string Generate(); + } +} diff --git a/PettingZoo.Core/PettingZoo.Core.csproj b/PettingZoo.Core/PettingZoo.Core.csproj new file mode 100644 index 0000000..e37010b --- /dev/null +++ b/PettingZoo.Core/PettingZoo.Core.csproj @@ -0,0 +1,28 @@ + + + + net6.0 + enable + 0.1 + + + + + + + + + True + True + MessagePropertiesRendererStrings.resx + + + + + + ResXFileCodeGenerator + MessagePropertiesRendererStrings.Designer.cs + + + + diff --git a/Model/MessageBodyRenderer.cs b/PettingZoo.Core/Rendering/MessageBodyRenderer.cs similarity index 66% rename from Model/MessageBodyRenderer.cs rename to PettingZoo.Core/Rendering/MessageBodyRenderer.cs index 1ba2e62..f3d4128 100644 --- a/Model/MessageBodyRenderer.cs +++ b/PettingZoo.Core/Rendering/MessageBodyRenderer.cs @@ -3,25 +3,23 @@ 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 } }; - public static string Render(byte[] body, string contentType = "") + public static string Render(byte[] body, string? contentType) { - Func handler; - - if (ContentTypeHandlers.TryGetValue(contentType, out handler)) - return handler(body); + return (contentType != null) && 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.Core/Rendering/MessagePropertiesRenderer.cs b/PettingZoo.Core/Rendering/MessagePropertiesRenderer.cs new file mode 100644 index 0000000..2066430 --- /dev/null +++ b/PettingZoo.Core/Rendering/MessagePropertiesRenderer.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using PettingZoo.Core.Connection; + +namespace PettingZoo.Core.Rendering +{ + public class MessagePropertiesRenderer + { + public static IDictionary Render(MessageProperties properties) + { + var result = new Dictionary(); + + if (properties.AppId != null) + result.Add(MessagePropertiesRendererStrings.AppId, properties.AppId); + + if (properties.ContentEncoding != null) + result.Add(MessagePropertiesRendererStrings.ContentEncoding, properties.ContentEncoding); + + if (properties.ContentType != null) + result.Add(MessagePropertiesRendererStrings.ContentType, properties.ContentType); + + if (properties.CorrelationId != null) + result.Add(MessagePropertiesRendererStrings.CorrelationId, properties.CorrelationId); + + if (properties.DeliveryMode != null) + result.Add(MessagePropertiesRendererStrings.DeliveryMode, + properties.DeliveryMode == MessageDeliveryMode.Persistent + ? MessagePropertiesRendererStrings.DeliveryModePersistent + : MessagePropertiesRendererStrings.DeliveryModeNonPersistent); + + if (properties.Expiration != null) + result.Add(MessagePropertiesRendererStrings.Expiration, properties.Expiration); + + if (properties.MessageId != null) + result.Add(MessagePropertiesRendererStrings.MessageId, properties.MessageId); + + if (properties.Priority != null) + result.Add(MessagePropertiesRendererStrings.Priority, properties.Priority.Value.ToString()); + + if (properties.ReplyTo != null) + result.Add(MessagePropertiesRendererStrings.ReplyTo, properties.ReplyTo); + + if (properties.Timestamp != null) + result.Add(MessagePropertiesRendererStrings.Timestamp, properties.Timestamp.Value.ToString("G")); + + if (properties.Type != null) + result.Add(MessagePropertiesRendererStrings.Type, properties.Type); + + if (properties.UserId != null) + result.Add(MessagePropertiesRendererStrings.UserId, properties.UserId); + + foreach (var (key, value) in properties.Headers) + { + if (!result.TryAdd(key, value)) + result.TryAdd(MessagePropertiesRendererStrings.HeaderPrefix + key, value); + } + + return result; + } + } +} diff --git a/PettingZoo.Core/Rendering/MessagePropertiesRendererStrings.Designer.cs b/PettingZoo.Core/Rendering/MessagePropertiesRendererStrings.Designer.cs new file mode 100644 index 0000000..7125e30 --- /dev/null +++ b/PettingZoo.Core/Rendering/MessagePropertiesRendererStrings.Designer.cs @@ -0,0 +1,198 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace PettingZoo.Core.Rendering { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // 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", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class MessagePropertiesRendererStrings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal MessagePropertiesRendererStrings() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.Core.Rendering.MessagePropertiesRendererStrings", typeof(MessagePropertiesRendererStrings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to App ID. + /// + internal static string AppId { + get { + return ResourceManager.GetString("AppId", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Content encoding. + /// + internal static string ContentEncoding { + get { + return ResourceManager.GetString("ContentEncoding", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Content type. + /// + internal static string ContentType { + get { + return ResourceManager.GetString("ContentType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Correlation ID. + /// + internal static string CorrelationId { + get { + return ResourceManager.GetString("CorrelationId", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Delivery mode. + /// + internal static string DeliveryMode { + get { + return ResourceManager.GetString("DeliveryMode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Transient (1). + /// + internal static string DeliveryModeNonPersistent { + get { + return ResourceManager.GetString("DeliveryModeNonPersistent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Persistent (2). + /// + internal static string DeliveryModePersistent { + get { + return ResourceManager.GetString("DeliveryModePersistent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Expiration. + /// + internal static string Expiration { + get { + return ResourceManager.GetString("Expiration", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Header: . + /// + internal static string HeaderPrefix { + get { + return ResourceManager.GetString("HeaderPrefix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Message ID. + /// + internal static string MessageId { + get { + return ResourceManager.GetString("MessageId", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Priority. + /// + internal static string Priority { + get { + return ResourceManager.GetString("Priority", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Reply To. + /// + internal static string ReplyTo { + get { + return ResourceManager.GetString("ReplyTo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Timestamp. + /// + internal static string Timestamp { + get { + return ResourceManager.GetString("Timestamp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type. + /// + internal static string Type { + get { + return ResourceManager.GetString("Type", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User ID. + /// + internal static string UserId { + get { + return ResourceManager.GetString("UserId", resourceCulture); + } + } + } +} diff --git a/PettingZoo.Core/Rendering/MessagePropertiesRendererStrings.resx b/PettingZoo.Core/Rendering/MessagePropertiesRendererStrings.resx new file mode 100644 index 0000000..ceb26c1 --- /dev/null +++ b/PettingZoo.Core/Rendering/MessagePropertiesRendererStrings.resx @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + App ID + + + Content encoding + + + Content type + + + Correlation ID + + + Delivery mode + + + Transient (1) + + + Persistent (2) + + + Expiration + + + Header: + + + Message ID + + + Priority + + + Reply To + + + Timestamp + + + Type + + + User ID + + \ No newline at end of file diff --git a/PettingZoo.Core/Settings/IConnectionSettingsRepository.cs b/PettingZoo.Core/Settings/IConnectionSettingsRepository.cs new file mode 100644 index 0000000..a5350c7 --- /dev/null +++ b/PettingZoo.Core/Settings/IConnectionSettingsRepository.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace PettingZoo.Core.Settings +{ + public interface IConnectionSettingsRepository + { + Task GetLastUsed(); + Task StoreLastUsed(bool storePassword, ConnectionSettings connectionSettings); + + Task> GetStored(); + Task Add(string displayName, bool storePassword, ConnectionSettings connectionSettings); + Task Update(Guid id, string displayName, bool storePassword, ConnectionSettings connectionSettings); + Task Delete(Guid id); + } + + + public class ConnectionSettings + { + public string Host { get; } + public string VirtualHost { get; } + public int Port { get; } + public string Username { get; } + public string Password { get; } + + public bool Subscribe { get; } + public string Exchange { get; } + public string RoutingKey { get; } + + + public static readonly ConnectionSettings Default = new("localhost", "/", 5672, "guest", "guest", false, "", "#"); + + + public ConnectionSettings(string host, string virtualHost, int port, string username, string password, + bool subscribe, string exchange, string routingKey) + { + Host = host; + VirtualHost = virtualHost; + Port = port; + Username = username; + Password = password; + + Subscribe = subscribe; + Exchange = exchange; + RoutingKey = routingKey; + } + + + public bool SameParameters(ConnectionSettings value, bool comparePassword = true) + { + return Host == value.Host && + VirtualHost == value.VirtualHost && + Port == value.Port && + Username == value.Username && + (!comparePassword || Password == value.Password) && + Subscribe == value.Subscribe && + Exchange == value.Exchange && + RoutingKey == value.RoutingKey; + } + } + + + public class StoredConnectionSettings : ConnectionSettings + { + public Guid Id { get; } + public string DisplayName { get; } + public bool StorePassword { get; } + + + public StoredConnectionSettings(Guid id, string displayName, bool storePassword, string host, string virtualHost, int port, string username, + string password, bool subscribe, string exchange, string routingKey) + : base(host, virtualHost, port, username, password, subscribe, exchange, routingKey) + { + Id = id; + DisplayName = displayName; + StorePassword = storePassword; + } + + + public StoredConnectionSettings(Guid id, string displayName, bool storePassword, ConnectionSettings connectionSettings) + : base(connectionSettings.Host, connectionSettings.VirtualHost, connectionSettings.Port, connectionSettings.Username, + connectionSettings.Password, connectionSettings.Subscribe, connectionSettings.Exchange, connectionSettings.RoutingKey) + { + Id = id; + DisplayName = displayName; + StorePassword = storePassword; + } + } +} \ No newline at end of file diff --git a/PettingZoo.Core/Settings/IUISettingsRepository.cs b/PettingZoo.Core/Settings/IUISettingsRepository.cs new file mode 100644 index 0000000..d32aaee --- /dev/null +++ b/PettingZoo.Core/Settings/IUISettingsRepository.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; + +namespace PettingZoo.Core.Settings +{ + public interface IUISettingsRepository + { + Task GetMainWindowPosition(); + Task StoreMainWindowPosition(MainWindowPositionSettings settings); + } + + + public class MainWindowPositionSettings + { + public int Top { get; } + public int Left { get; } + public int Width { get; } + public int Height { get; } + public bool Maximized { get; } + + + public MainWindowPositionSettings(int top, int left, int width, int height, bool maximized) + { + Top = top; + Left = left; + Width = width; + Height = height; + Maximized = maximized; + } + } +} diff --git a/PettingZoo.RabbitMQ/PettingZoo.RabbitMQ.csproj b/PettingZoo.RabbitMQ/PettingZoo.RabbitMQ.csproj new file mode 100644 index 0000000..7a65453 --- /dev/null +++ b/PettingZoo.RabbitMQ/PettingZoo.RabbitMQ.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + 0.1 + + + + + + + + + + + + diff --git a/PettingZoo.RabbitMQ/RabbitMQClientConnection.cs b/PettingZoo.RabbitMQ/RabbitMQClientConnection.cs new file mode 100644 index 0000000..9f790c8 --- /dev/null +++ b/PettingZoo.RabbitMQ/RabbitMQClientConnection.cs @@ -0,0 +1,156 @@ +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; + } + } + + GC.SuppressFinalize(this); + } + + + public ISubscriber Subscribe(string exchange, string routingKey) + { + return CreateSubscriber(exchange, routingKey); + } + + + public ISubscriber Subscribe() + { + return CreateSubscriber(null, null); + } + + + private ISubscriber CreateSubscriber(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(PublishMessageInfo 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..8ad2be5 --- /dev/null +++ b/PettingZoo.RabbitMQ/RabbitMQClientPropertiesConverter.cs @@ -0,0 +1,137 @@ +using System; +using System.Linq; +using System.Text; +using PettingZoo.Core.Connection; +using RabbitMQ.Client; + +namespace PettingZoo.RabbitMQ +{ + public static class RabbitMQClientPropertiesConverter + { + public static MessageProperties Convert(IBasicProperties basicProperties) + { + return new MessageProperties(basicProperties.Headers?.ToDictionary(p => p.Key, p => Encoding.UTF8.GetString((byte[])p.Value))) + { + DeliveryMode = basicProperties.IsDeliveryModePresent() + ? basicProperties.DeliveryMode == 2 ? MessageDeliveryMode.Persistent : + MessageDeliveryMode.NonPersistent + : null, + + ContentType = basicProperties.IsContentTypePresent() + ? basicProperties.ContentType + : null, + + ContentEncoding = basicProperties.IsContentEncodingPresent() + ? basicProperties.ContentEncoding + : null, + + Priority = basicProperties.IsPriorityPresent() + ? basicProperties.Priority + : null, + + CorrelationId = basicProperties.IsCorrelationIdPresent() + ? basicProperties.CorrelationId + : null, + + ReplyTo = basicProperties.IsReplyToPresent() + ? basicProperties.ReplyTo + : null, + + Expiration = basicProperties.IsExpirationPresent() + ? basicProperties.Expiration + : null, + + MessageId = basicProperties.IsMessageIdPresent() + ? basicProperties.MessageId + : null, + + Timestamp = basicProperties.IsTimestampPresent() + ? DateTimeOffset.FromUnixTimeMilliseconds(basicProperties.Timestamp.UnixTime).LocalDateTime + : null, + + Type = basicProperties.IsTypePresent() + ? basicProperties.Type + : null, + + UserId = basicProperties.IsUserIdPresent() + ? basicProperties.UserId + : null, + + AppId = basicProperties.IsAppIdPresent() + ? basicProperties.AppId + : null + }; + } + + + public static IBasicProperties Convert(MessageProperties properties, IBasicProperties targetProperties) + { + if (properties.DeliveryMode != null) + targetProperties.DeliveryMode = properties.DeliveryMode == MessageDeliveryMode.Persistent ? (byte)2 : (byte)1; + else + targetProperties.ClearDeliveryMode(); + + if (properties.ContentType != null) + targetProperties.ContentType = properties.ContentType; + else + targetProperties.ClearContentType(); + + if (properties.ContentEncoding != null) + targetProperties.ContentEncoding = properties.ContentEncoding; + else + targetProperties.ClearContentEncoding(); + + if (properties.Priority != null) + targetProperties.Priority = properties.Priority.Value; + else + targetProperties.ClearPriority(); + + if (properties.CorrelationId != null) + targetProperties.CorrelationId = properties.CorrelationId; + else + targetProperties.ClearCorrelationId(); + + if (properties.ReplyTo != null) + targetProperties.ReplyTo = properties.ReplyTo; + else + targetProperties.ClearReplyTo(); + + if (properties.Expiration != null) + targetProperties.Expiration = properties.Expiration; + else + targetProperties.ClearExpiration(); + + if (properties.MessageId != null) + targetProperties.MessageId = properties.MessageId; + else + targetProperties.ClearMessageId(); + + if (properties.Timestamp != null) + targetProperties.Timestamp = new AmqpTimestamp(new DateTimeOffset(properties.Timestamp.Value).ToUnixTimeMilliseconds()); + else + targetProperties.ClearTimestamp(); + + if (properties.Type != null) + targetProperties.Type = properties.Type; + else + targetProperties.ClearType(); + + if (properties.UserId != null) + targetProperties.UserId = properties.UserId; + else + targetProperties.ClearUserId(); + + if (properties.AppId != null) + targetProperties.AppId = properties.AppId; + else + targetProperties.ClearAppId(); + + if (properties.Headers.Count > 0) + targetProperties.Headers = properties.Headers.ToDictionary(p => p.Key, p => (object)Encoding.UTF8.GetBytes(p.Value)); + else + targetProperties.ClearHeaders(); + + return targetProperties; + } + } +} diff --git a/PettingZoo.RabbitMQ/RabbitMQClientSubscriber.cs b/PettingZoo.RabbitMQ/RabbitMQClientSubscriber.cs new file mode 100644 index 0000000..2c24db9 --- /dev/null +++ b/PettingZoo.RabbitMQ/RabbitMQClientSubscriber.cs @@ -0,0 +1,83 @@ +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? QueueName { get; private set; } + 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() + { + GC.SuppressFinalize(this); + + if (model != null && consumerTag != null && model.IsOpen) + model.BasicCancelNoWait(consumerTag); + + return default; + } + + + public void Start() + { + started = true; + if (model == null) + return; + + QueueName = model.QueueDeclare().QueueName; + if (Exchange != null && RoutingKey != null) + 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 ReceivedMessageInfo( + 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/PettingZoo.Settings.LiteDB/BaseLiteDBRepository.cs b/PettingZoo.Settings.LiteDB/BaseLiteDBRepository.cs new file mode 100644 index 0000000..f255b99 --- /dev/null +++ b/PettingZoo.Settings.LiteDB/BaseLiteDBRepository.cs @@ -0,0 +1,35 @@ +using LiteDB; +using LiteDB.Async; + +namespace PettingZoo.Settings.LiteDB +{ + public class BaseLiteDBRepository + { + private readonly string databaseFilename; + + protected static readonly BsonMapper Mapper = new() + { + EmptyStringToNull = false + }; + + + public BaseLiteDBRepository(string databaseName) + { + var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + if (appDataPath == null) + throw new IOException("Could not resolve application data path"); + + var databasePath = Path.Combine(appDataPath, @"PettingZoo"); + if (!Directory.CreateDirectory(databasePath).Exists) + throw new IOException($"Failed to create directory: {databasePath}"); + + databaseFilename = Path.Combine(databasePath, $"{databaseName}.litedb"); + } + + + protected ILiteDatabaseAsync GetDatabase() + { + return new LiteDatabaseAsync(databaseFilename, Mapper); + } + } +} diff --git a/PettingZoo.Settings.LiteDB/LiteDBConnectionSettingsRepository.cs b/PettingZoo.Settings.LiteDB/LiteDBConnectionSettingsRepository.cs new file mode 100644 index 0000000..41f237c --- /dev/null +++ b/PettingZoo.Settings.LiteDB/LiteDBConnectionSettingsRepository.cs @@ -0,0 +1,133 @@ +using PettingZoo.Core.Settings; + +namespace PettingZoo.Settings.LiteDB +{ + public class LiteDBConnectionSettingsRepository : BaseLiteDBRepository, IConnectionSettingsRepository + { + private static readonly Guid LastUsedId = new("1624147f-76b2-4b5e-8e6f-2ef1730a0a99"); + + private const string CollectionLastUsed = "lastUsed"; + private const string CollectionStored = "stored"; + + + public LiteDBConnectionSettingsRepository() : base(@"connectionSettings") + { + } + + + public async Task GetLastUsed() + { + using var database = GetDatabase(); + var collection = database.GetCollection(CollectionLastUsed); + + var lastUsed = await collection.FindOneAsync(r => true); + if (lastUsed == null) + return new StoredConnectionSettings(Guid.Empty, "", true, ConnectionSettings.Default); + + return new StoredConnectionSettings( + Guid.Empty, + "", + lastUsed.Password != null, + lastUsed.Host, + lastUsed.VirtualHost, + lastUsed.Port, + lastUsed.Username, + lastUsed.Password ?? "", + lastUsed.Subscribe, + lastUsed.Exchange, + lastUsed.RoutingKey); + } + + + public async Task StoreLastUsed(bool storePassword, ConnectionSettings connectionSettings) + { + using var database = GetDatabase(); + var collection = database.GetCollection(CollectionLastUsed); + + await collection.UpsertAsync(ConnectionSettingsRecord.FromConnectionSettings(LastUsedId, connectionSettings, "", storePassword)); + } + + + public async Task> GetStored() + { + using var database = GetDatabase(); + var collection = database.GetCollection(CollectionStored); + + return (await collection.FindAllAsync()) + .Select(r => new StoredConnectionSettings(r.Id, r.DisplayName, r.Password != null, r.Host, r.VirtualHost, r.Port, r.Username, r.Password ?? "", r.Subscribe, r.Exchange, r.RoutingKey)) + .ToArray(); + } + + + public async Task Add(string displayName, bool storePassword, ConnectionSettings connectionSettings) + { + using var database = GetDatabase(); + var collection = database.GetCollection(CollectionStored); + + var id = Guid.NewGuid(); + await collection.InsertAsync(ConnectionSettingsRecord.FromConnectionSettings(id, connectionSettings, displayName, storePassword)); + + return new StoredConnectionSettings(id, displayName, storePassword, connectionSettings); + } + + + public async Task Update(Guid id, string displayName, bool storePassword, ConnectionSettings connectionSettings) + { + using var database = GetDatabase(); + var collection = database.GetCollection(CollectionStored); + + await collection.UpdateAsync(ConnectionSettingsRecord.FromConnectionSettings(id, connectionSettings, displayName, storePassword)); + return new StoredConnectionSettings(id, displayName, storePassword, connectionSettings); + } + + + public async Task Delete(Guid id) + { + using var database = GetDatabase(); + var collection = database.GetCollection(CollectionStored); + + await collection.DeleteAsync(id); + } + + + // ReSharper disable MemberCanBePrivate.Local - for LiteDB + // ReSharper disable PropertyCanBeMadeInitOnly.Local + private class ConnectionSettingsRecord + { + public Guid Id { get; set; } + + public string DisplayName { get; set; } = null!; + public string Host { get; set; } = null!; + public string VirtualHost { get; set; } = null!; + public int Port { get; set; } + public string Username { get; set; } = null!; + public string? Password { get; set; } + + public bool Subscribe { get; set; } + public string Exchange { get; set; } = null!; + public string RoutingKey { get; set; } = null!; + + + public static ConnectionSettingsRecord FromConnectionSettings(Guid id, ConnectionSettings connectionSettings, string displayName, bool storePassword) + { + return new ConnectionSettingsRecord + { + Id = id, + DisplayName = displayName, + + Host = connectionSettings.Host, + VirtualHost = connectionSettings.VirtualHost, + Port = connectionSettings.Port, + Username = connectionSettings.Username, + Password = storePassword ? connectionSettings.Password : null, + + Subscribe = connectionSettings.Subscribe, + Exchange = connectionSettings.Exchange, + RoutingKey = connectionSettings.RoutingKey + }; + } + } + // ReSharper restore PropertyCanBeMadeInitOnly.Local + // ReSharper restore MemberCanBePrivate.Local + } +} \ No newline at end of file diff --git a/PettingZoo.Settings.LiteDB/LiteDBUISettingsRepository.cs b/PettingZoo.Settings.LiteDB/LiteDBUISettingsRepository.cs new file mode 100644 index 0000000..e7f54da --- /dev/null +++ b/PettingZoo.Settings.LiteDB/LiteDBUISettingsRepository.cs @@ -0,0 +1,84 @@ +using PettingZoo.Core.Settings; + +namespace PettingZoo.Settings.LiteDB +{ + public class LiteDBUISettingsRepository : BaseLiteDBRepository, IUISettingsRepository + { + private const string CollectionSettings = "settings"; + + + public LiteDBUISettingsRepository() : base(@"uiSettings") + { + } + + + public async Task GetMainWindowPosition() + { + using var database = GetDatabase(); + var collection = database.GetCollection(CollectionSettings); + + var settings = await collection.FindByIdAsync(MainWindowPositionSettingsRecord.SettingsKey); + if (settings == null) + return null; + + var position = Mapper.ToObject(settings); + return new MainWindowPositionSettings( + position.Top, + position.Left, + position.Width, + position.Height, + position.Maximized); + } + + + public async Task StoreMainWindowPosition(MainWindowPositionSettings settings) + { + using var database = GetDatabase(); + var collection = database.GetCollection(CollectionSettings); + + await collection.UpsertAsync( + Mapper.ToDocument(new MainWindowPositionSettingsRecord + { + Top = settings.Top, + Left = settings.Left, + Width = settings.Width, + Height = settings.Height, + Maximized = settings.Maximized + })); + } + + + + // ReSharper disable MemberCanBePrivate.Local - for LiteDB + // ReSharper disable PropertyCanBeMadeInitOnly.Local + private class BaseSettingsRecord + { + // ReSharper disable once UnusedAutoPropertyAccessor.Local + public Guid Id { get; } + + protected BaseSettingsRecord(Guid id) + { + Id = id; + } + } + + + private class MainWindowPositionSettingsRecord : BaseSettingsRecord + { + public static readonly Guid SettingsKey = new("fc71cf99-0744-4f5d-ada8-6a78d1df7b62"); + + + public int Top { get; set; } + public int Left { get; set; } + public int Width { get; set; } + public int Height { get; set; } + public bool Maximized { get; set; } + + public MainWindowPositionSettingsRecord() : base(SettingsKey) + { + } + } + // ReSharper restore PropertyCanBeMadeInitOnly.Local + // ReSharper restore MemberCanBePrivate.Local + } +} diff --git a/PettingZoo.Settings.LiteDB/PettingZoo.Settings.LiteDB.csproj b/PettingZoo.Settings.LiteDB/PettingZoo.Settings.LiteDB.csproj new file mode 100644 index 0000000..d6532ad --- /dev/null +++ b/PettingZoo.Settings.LiteDB/PettingZoo.Settings.LiteDB.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + diff --git a/PettingZoo.Tapeti/PettingZoo.Tapeti.csproj b/PettingZoo.Tapeti/PettingZoo.Tapeti.csproj new file mode 100644 index 0000000..d9b83bb --- /dev/null +++ b/PettingZoo.Tapeti/PettingZoo.Tapeti.csproj @@ -0,0 +1,16 @@ + + + + net6.0 + 0.1 + + + + + + + + + + + diff --git a/PettingZoo.Tapeti/TapetiClassLibraryExampleSource.cs b/PettingZoo.Tapeti/TapetiClassLibraryExampleSource.cs new file mode 100644 index 0000000..ec91428 --- /dev/null +++ b/PettingZoo.Tapeti/TapetiClassLibraryExampleSource.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using Newtonsoft.Json; +using PettingZoo.Core.Generator; + +namespace PettingZoo.Tapeti +{ + public class TapetiClassLibraryExampleSource : IExampleSource + { + private readonly string classLibraryFilename; + private readonly IEnumerable extraAssemblies; + private Lazy assemblySource; + + + public TapetiClassLibraryExampleSource(string classLibraryFilename, IEnumerable extraAssemblies) + { + this.classLibraryFilename = classLibraryFilename; + this.extraAssemblies = extraAssemblies; + + assemblySource = new Lazy(AssemblySourceFactory); + } + + + public void Dispose() + { + if (assemblySource.IsValueCreated) + assemblySource.Value.Dispose(); + + GC.SuppressFinalize(this); + } + + + public IExampleFolder GetRootFolder() + { + return assemblySource.Value.RootFolder; + } + + + private AssemblySource AssemblySourceFactory() + { + var runtimeAssemblies = Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll"); + + var paths = runtimeAssemblies + .Concat(extraAssemblies) + .Append(classLibraryFilename); + + // TODO can we use a custom resolver to detect missing references? + var resolver = new PathAssemblyResolver(paths); + var loadContext = new MetadataLoadContext(resolver); + try + { + var assembly = loadContext.LoadFromAssemblyPath(classLibraryFilename); + var rootFolder = new Folder(@"Root"); + + + foreach (var assemblyType in assembly.GetTypes()) + AddType(assemblyType, rootFolder); + + + return new AssemblySource + { + LoadContext = loadContext, + RootFolder = rootFolder + }; + } + catch + { + loadContext.Dispose(); + throw; + } + } + + + private void AddType(Type type, Folder rootFolder) + { + if (!type.IsClass) + return; + + var assemblyName = type.Assembly.GetName().Name + "."; + var typeNamespace = type.Namespace ?? ""; + + if (typeNamespace.StartsWith(assemblyName)) + typeNamespace = typeNamespace.Substring(assemblyName.Length); + + var folder = CreateFolder(rootFolder, typeNamespace); + folder.AddMessage(new Message(type)); + } + + + private static Folder CreateFolder(Folder rootFolder, string typeNamespace) + { + var parts = typeNamespace.Split('.'); + if (parts.Length == 0) + return rootFolder; + + var folder = rootFolder; + + foreach (var part in parts) + folder = folder.CreateFolder(part); + + return folder; + } + + + private class Folder : IExampleFolder + { + private readonly List folders = new(); + private readonly List messages = new(); + + + public string Name { get; } + public IReadOnlyList Folders => folders; + public IReadOnlyList Messages => messages; + + + public Folder(string name) + { + Name = name; + } + + + public Folder CreateFolder(string name) + { + var folder = folders.FirstOrDefault(f => f.Name == name); + if (folder != null) + return folder; + + folder = new Folder(name); + folders.Add(folder); + return folder; + } + + + public void AddMessage(IExampleMessage message) + { + messages.Add(message); + } + } + + + private class Message : IExampleMessage + { + private readonly Type type; + + + public Message(Type type) + { + this.type = type; + } + + + public string Generate() + { + /* + We can't create an instance of the type to serialize easily, as most will depend on + assemblies not included in the NuGet package, so we'll parse the Type ourselves. + This is still much easier than using MetadataReader, as we can more easily check against + standard types like Nullable. + + The only external dependencies should be the attributes, like [RequiredGuid]. The messaging models + themselves should not inherit from classes outside of their assembly, or include properties + with types from other assemblies. With that assumption, walking the class structure should be safe. + The extraAssemblies passed to TapetiClassLibraryExampleSource can also be used to give it a better chance. + */ + var serialized = TypeToJObjectConverter.Convert(type); + return serialized.ToString(Formatting.Indented); + } + } + + + private class AssemblySource : IDisposable + { + public MetadataLoadContext LoadContext { get; init; } + public IExampleFolder RootFolder { get; init; } + + + public void Dispose() + { + LoadContext.Dispose(); + } + } + } +} diff --git a/PettingZoo.Tapeti/TypeToJObjectConverter.cs b/PettingZoo.Tapeti/TypeToJObjectConverter.cs new file mode 100644 index 0000000..d807501 --- /dev/null +++ b/PettingZoo.Tapeti/TypeToJObjectConverter.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Newtonsoft.Json.Linq; + +namespace PettingZoo.Tapeti +{ + internal class TypeToJObjectConverter + { + public static JObject Convert(Type type) + { + var result = new JObject(); + + foreach (var propertyInfo in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + // Note: unfortunately we can not call GetCustomAttributes here, as that would + // trigger assemblies not included in the package to be loaded + + var value = PropertyToJToken(propertyInfo.PropertyType); + result.Add(propertyInfo.Name, value); + } + + return result; + } + + + private static readonly Dictionary TypeMap = new() + { + { typeof(short), 0 }, + { typeof(ushort), 0 }, + { typeof(int), 0 }, + { typeof(uint), 0 }, + { typeof(long), 0 }, + { typeof(ulong), 0 }, + { typeof(decimal), 0.0 }, + { typeof(float), 0.0 }, + { typeof(bool), false } + }; + + + private static JToken PropertyToJToken(Type propertyType) + { + var actualType = Nullable.GetUnderlyingType(propertyType) ?? propertyType; + + + // String is also a class + if (actualType == typeof(string)) + return ""; + + + if (actualType.IsClass) + { + // IEnumerable + var enumerableInterface = actualType.GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)); + + if (enumerableInterface != null) + return new JArray(Convert(enumerableInterface.GetGenericArguments()[0])); + + + return Convert(actualType); + } + + if (actualType.IsArray) + return new JArray(Convert(actualType.GetElementType())); + + if (actualType.IsEnum) + return Enum.GetNames(actualType).FirstOrDefault(); + + + // Special cases for runtime generated values + if (actualType == typeof(DateTime)) + { + // Strip the milliseconds for a cleaner result + var now = DateTime.UtcNow; + return new DateTime(now.Ticks - now.Ticks % TimeSpan.TicksPerSecond, now.Kind); + } + + if (actualType == typeof(Guid)) + return Guid.NewGuid().ToString(); + + return TypeMap.TryGetValue(actualType, out var mappedToken) + ? mappedToken + : $"(unknown type: {actualType.Name})"; + } + + + } +} 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..66d5304 100644 --- a/PettingZoo.sln +++ b/PettingZoo.sln @@ -1,9 +1,22 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.40629.0 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31912.275 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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.Core", "PettingZoo.Core\PettingZoo.Core.csproj", "{AD20CA14-6272-4C50-819D-F9FE6A963DB1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.RabbitMQ", "PettingZoo.RabbitMQ\PettingZoo.RabbitMQ.csproj", "{220149F3-A8D6-44ED-B3B6-DFE506EB018A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.Tapeti", "PettingZoo.Tapeti\PettingZoo.Tapeti.csproj", "{1763AB04-59D9-4663-B207-D6302FFAACD5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.Settings.LiteDB", "PettingZoo.Settings.LiteDB\PettingZoo.Settings.LiteDB.csproj", "{7157B09C-FDD9-4928-B14D-C25B784CA865}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,8 +28,27 @@ 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 + {1763AB04-59D9-4663-B207-D6302FFAACD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1763AB04-59D9-4663-B207-D6302FFAACD5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1763AB04-59D9-4663-B207-D6302FFAACD5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1763AB04-59D9-4663-B207-D6302FFAACD5}.Release|Any CPU.Build.0 = Release|Any CPU + {7157B09C-FDD9-4928-B14D-C25B784CA865}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7157B09C-FDD9-4928-B14D-C25B784CA865}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7157B09C-FDD9-4928-B14D-C25B784CA865}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7157B09C-FDD9-4928-B14D-C25B784CA865}.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/PettingZoo.sln.DotSettings b/PettingZoo.sln.DotSettings index f8f70cf..e8372d6 100644 --- a/PettingZoo.sln.DotSettings +++ b/PettingZoo.sln.DotSettings @@ -1,4 +1,7 @@  True + DB + DBUI + UI MQ WPF \ No newline at end of file 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..40f08f2 --- /dev/null +++ b/PettingZoo/App.xaml.cs @@ -0,0 +1,86 @@ +using System; +using System.Drawing; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Threading; +using PettingZoo.Core.Settings; +using PettingZoo.UI.Main; +using SimpleInjector; +using Point = System.Windows.Point; + +namespace PettingZoo +{ + public partial class App + { + private readonly Container container; + + + public App() + { + throw new InvalidOperationException("Default main should not be used"); + } + + + public App(Container container) + { + this.container = container; + } + + + protected override async void OnStartup(StartupEventArgs e) + { + var uitSettingsRepository = container.GetInstance(); + var position = await uitSettingsRepository.GetMainWindowPosition(); + + var mainWindow = container.GetInstance(); + + if (position != null) + { + var positionBounds = new Rect( + new Point(position.Left, position.Top), + new Point(position.Left + position.Width, position.Top + position.Height)); + + if (InScreenBounds(positionBounds)) + { + mainWindow.WindowStartupLocation = WindowStartupLocation.Manual; + mainWindow.Top = positionBounds.Top; + mainWindow.Left = positionBounds.Left; + mainWindow.Width = positionBounds.Width; + mainWindow.Height = positionBounds.Height; + } + + mainWindow.WindowState = position.Maximized ? WindowState.Maximized : WindowState.Normal; + } + + mainWindow.Closing += (_, _) => + { + var newPosition = new MainWindowPositionSettings( + (int)mainWindow.RestoreBounds.Top, + (int)mainWindow.RestoreBounds.Left, + (int)mainWindow.RestoreBounds.Width, + (int)mainWindow.RestoreBounds.Height, + mainWindow.WasMaximized); + + Task.Run(() => uitSettingsRepository.StoreMainWindowPosition(newPosition)); + }; + + mainWindow.Show(); + } + + + private static bool InScreenBounds(Rect bounds) + { + var boundsRectangle = new Rectangle((int)bounds.Left, (int)bounds.Top, (int)bounds.Width, (int)bounds.Height); + + // There doesn't appear to be any way to get this information other than from System.Windows.From/PInvoke at the time of writing + return System.Windows.Forms.Screen.AllScreens.Any(screen => screen.Bounds.IntersectsWith(boundsRectangle)); + } + + + 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 88% rename from Images/Clear.svg rename to PettingZoo/Images/Clear.svg index a93f38f..16ad260 100644 --- a/Images/Clear.svg +++ b/PettingZoo/Images/Clear.svg @@ -1,5 +1,4 @@ - @@ -23,34 +22,4 @@ s0.512-0.098,0.707-0.293c0.391-0.391,0.391-1.023,0-1.414L45.914,46z"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Images/Connect.svg b/PettingZoo/Images/Connect.svg similarity index 93% rename from Images/Connect.svg rename to PettingZoo/Images/Connect.svg index cc7e08e..b832f73 100644 --- a/Images/Connect.svg +++ b/PettingZoo/Images/Connect.svg @@ -1,5 +1,4 @@ - @@ -52,34 +51,4 @@ c0.045,0,0.091-0.003,0.137-0.009c0.276-0.039,0.524-0.19,0.684-0.419l6.214-8.929C54.136,42.118,54.024,41.495,53.571,41.179z"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/PettingZoo/Images/Connecting.svg b/PettingZoo/Images/Connecting.svg new file mode 100644 index 0000000..1ddcdec --- /dev/null +++ b/PettingZoo/Images/Connecting.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + diff --git a/Images/Disconnect.svg b/PettingZoo/Images/Disconnect.svg similarity index 93% rename from Images/Disconnect.svg rename to PettingZoo/Images/Disconnect.svg index 34bc7c2..fef3029 100644 --- a/Images/Disconnect.svg +++ b/PettingZoo/Images/Disconnect.svg @@ -1,5 +1,4 @@ - @@ -53,34 +52,4 @@ s0.512-0.098,0.707-0.293c0.391-0.391,0.391-1.023,0-1.414L48.414,47z"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/PettingZoo/Images/Dock.svg b/PettingZoo/Images/Dock.svg new file mode 100644 index 0000000..bbdf38e --- /dev/null +++ b/PettingZoo/Images/Dock.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + diff --git a/PettingZoo/Images/Error.svg b/PettingZoo/Images/Error.svg new file mode 100644 index 0000000..89d9213 --- /dev/null +++ b/PettingZoo/Images/Error.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PettingZoo/Images/Ok.svg b/PettingZoo/Images/Ok.svg new file mode 100644 index 0000000..d15248f --- /dev/null +++ b/PettingZoo/Images/Ok.svg @@ -0,0 +1,50 @@ + + + + + + + + + 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..1702435 --- /dev/null +++ b/PettingZoo/Images/Publish.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PettingZoo/Images/PublishSend.svg b/PettingZoo/Images/PublishSend.svg new file mode 100644 index 0000000..3cd7193 --- /dev/null +++ b/PettingZoo/Images/PublishSend.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/PettingZoo/Images/Subscribe.svg b/PettingZoo/Images/Subscribe.svg new file mode 100644 index 0000000..0654459 --- /dev/null +++ b/PettingZoo/Images/Subscribe.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/PettingZoo/Images/Undock.svg b/PettingZoo/Images/Undock.svg new file mode 100644 index 0000000..1b7cbad --- /dev/null +++ b/PettingZoo/Images/Undock.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + diff --git a/PettingZoo/PettingZoo.csproj b/PettingZoo/PettingZoo.csproj new file mode 100644 index 0000000..cc761f0 --- /dev/null +++ b/PettingZoo/PettingZoo.csproj @@ -0,0 +1,190 @@ + + + + WinExe + net6.0-windows + 0.1 + true + true + Mark van Renswoude + Petting Zoo + Petting Zoo - a live RabbitMQ message viewer + + enable + true + PettingZoo.Program + + + PettingZoo.ico + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + ConnectionDisplayNameStrings.resx + + + ConnectionWindowStrings.resx + True + True + + + True + True + ExamplePickerDialogStrings.resx + + + MainWindowStrings.resx + True + True + + + SubscribeWindowStrings.resx + True + True + + + True + True + PayloadEditorStrings.resx + + + TapetiPublisherViewStrings.resx + True + True + + + Code + + + RawPublisherViewStrings.resx + True + True + + + PublisherViewStrings.resx + True + True + + + SubscriberViewStrings.resx + True + True + + + True + True + UndockedTabHostStrings.resx + + + + + + PublicResXFileCodeGenerator + ConnectionDisplayNameStrings.Designer.cs + + + ConnectionWindowStrings.Designer.cs + PublicResXFileCodeGenerator + + + PublicResXFileCodeGenerator + ExamplePickerDialogStrings.Designer.cs + + + MainWindowStrings.Designer.cs + PublicResXFileCodeGenerator + + + SubscribeWindowStrings.Designer.cs + PublicResXFileCodeGenerator + + + ResXFileCodeGenerator + PayloadEditorStrings.Designer.cs + + + TapetiPublisherViewStrings.Designer.cs + PublicResXFileCodeGenerator + + + RawPublisherViewStrings.Designer.cs + PublicResXFileCodeGenerator + + + PublisherViewStrings.Designer.cs + PublicResXFileCodeGenerator + + + SubscriberViewStrings.Designer.cs + PublicResXFileCodeGenerator + + + PublicResXFileCodeGenerator + UndockedTabHostStrings.Designer.cs + + + + + + $(DefaultXamlRuntime) + + + $(DefaultXamlRuntime) + + + $(DefaultXamlRuntime) + + + $(DefaultXamlRuntime) + + + $(DefaultXamlRuntime) + Designer + + + $(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/PettingZoo/Program.cs b/PettingZoo/Program.cs new file mode 100644 index 0000000..f3e2dcf --- /dev/null +++ b/PettingZoo/Program.cs @@ -0,0 +1,78 @@ +using System; +using System.Globalization; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Markup; +using PettingZoo.Core.Connection; +using PettingZoo.Core.Settings; +using PettingZoo.RabbitMQ; +using PettingZoo.Settings.LiteDB; +using PettingZoo.UI.Connection; +using PettingZoo.UI.Main; +using PettingZoo.UI.Subscribe; +using SimpleInjector; + +namespace PettingZoo +{ + public static class Program + { + [STAThread] + public static void Main() + { + // WPF defaults to US for date formatting in bindings, this fixes it + FrameworkElement.LanguageProperty.OverrideMetadata(typeof(FrameworkElement), new FrameworkPropertyMetadata( + XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag))); + + var container = Bootstrap(); + RunApplication(container); + } + + + private static Container Bootstrap() + { + var container = new Container(); + + // See comments in RunApplication + container.Options.EnableAutoVerification = false; + + container.Register(); + container.Register(); + container.Register(); + container.Register(); + container.Register(); + + container.Register(); + + return container; + } + + + private static void RunApplication(Container container) + { + try + { + var app = new App(container); + app.InitializeComponent(); + + #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 + + app.Run(); + } + catch (Exception e) + { + MessageBox.Show($"Fatal exception: {e.Message}", @"PettingZoo", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + } +} \ No newline at end of file diff --git a/Style.xaml b/PettingZoo/Style.xaml similarity index 69% rename from Style.xaml rename to PettingZoo/Style.xaml index f5896b0..f26734e 100644 --- a/Style.xaml +++ b/PettingZoo/Style.xaml @@ -1,6 +1,6 @@  + xmlns:ui="clr-namespace:PettingZoo.UI"> + + + - - - + + + @@ -59,6 +68,25 @@ - + + + + + + + + \ No newline at end of file diff --git a/PettingZoo/TODO.md b/PettingZoo/TODO.md new file mode 100644 index 0000000..9657558 --- /dev/null +++ b/PettingZoo/TODO.md @@ -0,0 +1,13 @@ +Must-have +--------- + + +Should-have +----------- +- Save / load publisher messages (either as templates or to disk) +- Export received messages to Tapeti JSON file / Tapeti.Cmd command-line + + +Nice-to-have +------------ +- JSON syntax highlighting \ No newline at end of file diff --git a/PettingZoo/UI/BaseViewModel.cs b/PettingZoo/UI/BaseViewModel.cs new file mode 100644 index 0000000..af9108d --- /dev/null +++ b/PettingZoo/UI/BaseViewModel.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace PettingZoo.UI +{ + public class BaseViewModel : INotifyPropertyChanged + { + private int commandsChangedDisabled; + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void RaisePropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + + protected bool SetField(ref T field, T value, IEqualityComparer? comparer = null, [CallerMemberName] string? propertyName = null, + DelegateCommand[]? delegateCommandsChanged = null, + string[]? otherPropertiesChanged = null) + { + if ((comparer ?? EqualityComparer.Default).Equals(field, value)) + return false; + + field = value; + RaisePropertyChanged(propertyName); + + if (otherPropertiesChanged != null) + { + foreach (var otherProperty in otherPropertiesChanged) + RaisePropertyChanged(otherProperty); + } + + // ReSharper disable once InvertIf + if (delegateCommandsChanged != null) + { + foreach (var delegateCommand in delegateCommandsChanged) + delegateCommand.RaiseCanExecuteChanged(); + } + + return true; + } + + + protected void DisableCommandsChanged(Action updateFields, params DelegateCommand[] delegateCommandsChangedAfter) + { + commandsChangedDisabled++; + try + { + updateFields(); + } + finally + { + commandsChangedDisabled--; + if (commandsChangedDisabled == 0) + { + foreach (var delegateCommand in delegateCommandsChangedAfter) + delegateCommand.RaiseCanExecuteChanged(); + } + } + } + } +} \ No newline at end of file diff --git a/PettingZoo/UI/BindingProxy.cs b/PettingZoo/UI/BindingProxy.cs new file mode 100644 index 0000000..4bc6e38 --- /dev/null +++ b/PettingZoo/UI/BindingProxy.cs @@ -0,0 +1,24 @@ +using System.Windows; + +namespace PettingZoo.UI +{ + public class BindingProxy : Freezable + { + protected override Freezable CreateInstanceCore() + { + return new BindingProxy(); + } + + + public object Data + { + get => GetValue(DataProperty); + set => SetValue(DataProperty, value); + } + + + // Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc... + public static readonly DependencyProperty DataProperty = + DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null)); + } +} diff --git a/PettingZoo/UI/Connection/ConnectionDisplayNameDialog.xaml b/PettingZoo/UI/Connection/ConnectionDisplayNameDialog.xaml new file mode 100644 index 0000000..e76c9de --- /dev/null +++ b/PettingZoo/UI/Connection/ConnectionDisplayNameDialog.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PettingZoo/UI/Main/MainWindow.xaml.cs b/PettingZoo/UI/Main/MainWindow.xaml.cs new file mode 100644 index 0000000..fda439b --- /dev/null +++ b/PettingZoo/UI/Main/MainWindow.xaml.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.ObjectModel; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using PettingZoo.Core.Connection; +using PettingZoo.UI.Connection; +using PettingZoo.UI.Subscribe; +using PettingZoo.UI.Tab; + +namespace PettingZoo.UI.Main +{ + #pragma warning disable CA1001 // MainWindow can't be IDisposable, handled instead in OnDispatcherShutDownStarted + public partial class MainWindow : ITabContainer + { + private readonly MainWindowViewModel viewModel; + + public bool WasMaximized; + + + public MainWindow(IConnectionFactory connectionFactory, IConnectionDialog connectionDialog, ISubscribeDialog subscribeDialog) + { + WindowStartupLocation = WindowStartupLocation.CenterScreen; + + viewModel = new MainWindowViewModel(connectionFactory, connectionDialog, subscribeDialog, this); + DataContext = viewModel; + InitializeComponent(); + + Dispatcher.ShutdownStarted += OnDispatcherShutDownStarted; + + + // If the WindowState is Minimized, we can't tell if it was maximized before. To properly store + // the last window position, keep track of it. + this.OnPropertyChanges(WindowStateProperty) + .Subscribe(newState => + { + WasMaximized = newState switch + { + WindowState.Maximized => true, + WindowState.Normal => false, + _ => WasMaximized + }; + }); + } + + + 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; + } + + + private void TabItem_PreviewRightMouseDown(object sender, MouseButtonEventArgs e) + { + var tabItem = GetParent(e.OriginalSource); + if (tabItem == null) + return; + + var tabControl = GetParent(tabItem); + if (tabControl == null) + return; + + tabControl.SelectedItem = tabItem.DataContext; + } + + + private void TabItem_PreviewMouseMove(object sender, MouseEventArgs e) + { + if (e.Source is not TabItem tabItem) + return; + + if (Mouse.PrimaryDevice.LeftButton == MouseButtonState.Pressed) + DragDrop.DoDragDrop(tabItem, tabItem, DragDropEffects.All); + } + + + private void TabItem_Drop(object sender, DragEventArgs e) + { + var targetTab = GetParent(e.OriginalSource); + if (targetTab == null) + return; + + var sourceTab = (TabItem?)e.Data.GetData(typeof(TabItem)); + if (sourceTab == null || sourceTab == targetTab) + return; + + var tabControl = GetParent(targetTab); + if (tabControl?.ItemsSource is not ObservableCollection dataCollection) + return; + + if (sourceTab.DataContext is not ITab sourceData || targetTab.DataContext is not ITab targetData) + return; + + var sourceIndex = dataCollection.IndexOf(sourceData); + var targetIndex = dataCollection.IndexOf(targetData); + + dataCollection.Move(sourceIndex, targetIndex); + } + + + private static T? GetParent(object originalSource) where T : DependencyObject + { + var current = originalSource as DependencyObject; + + while (current != null) + { + if (current is T targetType) + return targetType; + + current = VisualTreeHelper.GetParent(current); + } + + return null; + } + + + public double TabWidth => SubscriberTabs.ActualWidth; + public double TabHeight => SubscriberTabs.ActualHeight; + + private void Toolbar_Loaded(object sender, RoutedEventArgs e) + { + // Hide arrow on the right side of the toolbar + var toolBar = sender as ToolBar; + + if (toolBar?.Template.FindName("OverflowGrid", toolBar) is FrameworkElement overflowGrid) + overflowGrid.Visibility = Visibility.Collapsed; + + if (toolBar?.Template.FindName("MainPanelBorder", toolBar) is FrameworkElement mainPanelBorder) + mainPanelBorder.Margin = new Thickness(0); + } + } + #pragma warning restore CA1001 +} diff --git a/Properties/Resources.Designer.cs b/PettingZoo/UI/Main/MainWindowStrings.Designer.cs similarity index 59% rename from Properties/Resources.Designer.cs rename to PettingZoo/UI/Main/MainWindowStrings.Designer.cs index 4b0bc29..a167f44 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", "17.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,65 @@ 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 Undock tab. /// - public static string ConnectionPassword { + public static string CommandUndock { get { - return ResourceManager.GetString("ConnectionPassword", resourceCulture); + return ResourceManager.GetString("CommandUndock", resourceCulture); } } /// - /// Looks up a localized string similar to Port:. + /// Looks up a localized string similar to Close tab. /// - public static string ConnectionPort { + public static string ContextMenuCloseTab { get { - return ResourceManager.GetString("ConnectionPort", resourceCulture); + return ResourceManager.GetString("ContextMenuCloseTab", resourceCulture); } } /// - /// Looks up a localized string similar to Routing key:. + /// Looks up a localized string similar to Undock. /// - public static string ConnectionRoutingKey { + public static string ContextMenuUndockTab { 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("ContextMenuUndockTab", resourceCulture); } } @@ -165,51 +141,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 +176,23 @@ namespace PettingZoo.Properties { return ResourceManager.GetString("StatusError", resourceCulture); } } + + /// + /// Looks up a localized string similar to No open tabs. Click on New Publisher or New Subscriber to open a new tab.. + /// + public static string TabsEmptyText { + get { + return ResourceManager.GetString("TabsEmptyText", 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..6814dea 100644 --- a/Properties/Resources.resx +++ b/PettingZoo/UI/Main/MainWindowStrings.resx @@ -117,35 +117,26 @@ 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: + + Undock tab - - Port: + + Close tab - - Routing key: - - - Username: - - - Virtual host: - - - Connection parameters + + Undock Non-persistent @@ -153,21 +144,6 @@ Persistent - - Petting Zoo - a RabbitMQ live message viewer - - - Body - - - Properties - - - Name - - - Value - Connected @@ -180,4 +156,10 @@ Error: {0} + + No open tabs. Click on New Publisher or New Subscriber to open a new tab. + + + 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..f8d4470 --- /dev/null +++ b/PettingZoo/UI/Main/MainWindowViewModel.cs @@ -0,0 +1,327 @@ +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; +using PettingZoo.UI.Tab.Undocked; + +namespace PettingZoo.UI.Main +{ + public enum ConnectionStatusType + { + Connecting, + Ok, + Error + } + + + public class MainWindowViewModel : BaseViewModel, IAsyncDisposable, ITabHost + { + private readonly IConnectionFactory connectionFactory; + private readonly IConnectionDialog connectionDialog; + private readonly ISubscribeDialog subscribeDialog; + private readonly ITabContainer tabContainer; + private readonly ITabFactory tabFactory; + + private SubscribeDialogParams? subscribeDialogParams; + private IConnection? connection; + private string connectionStatus; + private ITab? activeTab; + private readonly Dictionary undockedTabs = new(); + + private readonly DelegateCommand connectCommand; + private readonly DelegateCommand disconnectCommand; + private readonly DelegateCommand publishCommand; + private readonly DelegateCommand subscribeCommand; + private readonly DelegateCommand closeTabCommand; + private readonly DelegateCommand undockTabCommand; + + private ConnectionStatusType connectionStatusType; + + + public string ConnectionStatus + { + get => connectionStatus; + private set => SetField(ref connectionStatus, value); + } + + + public ConnectionStatusType ConnectionStatusType + { + get => connectionStatusType; + set => SetField(ref connectionStatusType, value, otherPropertiesChanged: new [] { nameof(ConnectionStatusOk), nameof(ConnectionStatusError), nameof(ConnectionStatusConnecting) }); + } + + public Visibility ConnectionStatusOk => ConnectionStatusType == ConnectionStatusType.Ok ? Visibility.Visible : Visibility.Collapsed; + public Visibility ConnectionStatusError => ConnectionStatusType == ConnectionStatusType.Error ? Visibility.Visible : Visibility.Collapsed; + public Visibility ConnectionStatusConnecting => ConnectionStatusType == ConnectionStatusType.Connecting ? Visibility.Visible : Visibility.Collapsed; + + + public ObservableCollection Tabs { get; } + + public ITab? ActiveTab + { + get => activeTab; + set + { + var currentTab = activeTab; + + if (!SetField(ref activeTab, value, otherPropertiesChanged: new[] { nameof(ToolbarCommands), nameof(ToolbarCommandsSeparatorVisibility) })) + return; + + currentTab?.Deactivate(); + activeTab?.Activate(); + } + } + + public ICommand ConnectCommand => connectCommand; + public ICommand DisconnectCommand => disconnectCommand; + public ICommand PublishCommand => publishCommand; + public ICommand SubscribeCommand => subscribeCommand; + public ICommand CloseTabCommand => closeTabCommand; + public ICommand UndockTabCommand => undockTabCommand; + + public IEnumerable ToolbarCommands => ActiveTab is ITabToolbarCommands tabToolbarCommands + ? tabToolbarCommands.ToolbarCommands + : Enumerable.Empty(); + + public Visibility ToolbarCommandsSeparatorVisibility => + ToolbarCommands.Any() ? Visibility.Visible : Visibility.Collapsed; + + public Visibility NoTabsVisibility => + Tabs.Count > 0 ? Visibility.Collapsed : Visibility.Visible; + + + public MainWindowViewModel(IConnectionFactory connectionFactory, IConnectionDialog connectionDialog, + ISubscribeDialog subscribeDialog, ITabContainer tabContainer) + { + this.connectionFactory = connectionFactory; + this.connectionDialog = connectionDialog; + this.subscribeDialog = subscribeDialog; + this.tabContainer = tabContainer; + + connectionStatus = GetConnectionStatus(null); + connectionStatusType = ConnectionStatusType.Error; + + 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, HasActiveTabCanExecute); + undockTabCommand = new DelegateCommand(UndockTabExecute, HasActiveTabCanExecute); + + tabFactory = new ViewTabFactory(this); + } + + + public async ValueTask DisposeAsync() + { + GC.SuppressFinalize(this); + + if (connection != null) + await connection.DisposeAsync(); + } + + + private async void ConnectExecute() + { + var connectionSettings = await connectionDialog.Show(); + if (connectionSettings == null) + return; + + if (connection != null) + await connection.DisposeAsync(); + + connection = connectionFactory.CreateConnection(new ConnectionParams( + connectionSettings.Host, connectionSettings.VirtualHost, connectionSettings.Port, + connectionSettings.Username, connectionSettings.Password)); + connection.StatusChanged += ConnectionStatusChanged; + + if (connectionSettings.Subscribe) + { + var subscriber = connection.Subscribe(connectionSettings.Exchange, connectionSettings.RoutingKey); + AddTab(tabFactory.CreateSubscriberTab(connection, subscriber)); + } + + ConnectionChanged(); + } + + + private async void DisconnectExecute() + { + Tabs.Clear(); + + var capturedUndockedTabs = undockedTabs.ToList(); + undockedTabs.Clear(); + + foreach (var undockedTab in capturedUndockedTabs) + undockedTab.Value.Close(); + + RaisePropertyChanged(nameof(NoTabsVisibility)); + undockTabCommand.RaiseCanExecuteChanged(); + + if (connection != null) + { + await connection.DisposeAsync(); + connection = null; + } + + ConnectionStatus = GetConnectionStatus(null); + ConnectionStatusType = ConnectionStatusType.Error; + 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(connection, subscriber)); + } + + + private void PublishExecute() + { + if (connection == null) + return; + + AddTab(tabFactory.CreatePublisherTab(connection)); + } + + + private bool IsConnectedCanExecute() + { + return connection != null; + } + + + private void CloseTabExecute() + { + RemoveActiveTab(); + } + + + private void UndockTabExecute() + { + var tab = RemoveActiveTab(); + if (tab == null) + return; + + var tabHostWindow = UndockedTabHostWindow.Create(this, tab, tabContainer.TabWidth, tabContainer.TabHeight); + undockedTabs.Add(tab, tabHostWindow); + + tabHostWindow.Show(); + } + + + private ITab? RemoveActiveTab() + { + if (ActiveTab == null) + return null; + + var activeTabIndex = Tabs.IndexOf(ActiveTab); + if (activeTabIndex == -1) + return null; + + var tab = Tabs[activeTabIndex]; + Tabs.RemoveAt(activeTabIndex); + + if (activeTabIndex == Tabs.Count) + activeTabIndex--; + + ActiveTab = activeTabIndex >= 0 ? Tabs[activeTabIndex] : null; + closeTabCommand.RaiseCanExecuteChanged(); + undockTabCommand.RaiseCanExecuteChanged(); + RaisePropertyChanged(nameof(NoTabsVisibility)); + + return tab; + } + + + private bool HasActiveTabCanExecute() + { + return ActiveTab != null; + } + + + public void AddTab(ITab tab) + { + Tabs.Add(tab); + ActiveTab = tab; + + closeTabCommand.RaiseCanExecuteChanged(); + undockTabCommand.RaiseCanExecuteChanged(); + RaisePropertyChanged(nameof(NoTabsVisibility)); + } + + + public void DockTab(ITab tab) + { + if (undockedTabs.Remove(tab, out var tabHostWindow)) + tabHostWindow.Close(); + + AddTab(tab); + } + + public void UndockedTabClosed(ITab tab) + { + undockedTabs.Remove(tab); + } + + + private void ConnectionChanged() + { + disconnectCommand.RaiseCanExecuteChanged(); + subscribeCommand.RaiseCanExecuteChanged(); + publishCommand.RaiseCanExecuteChanged(); + } + + private void ConnectionStatusChanged(object? sender, StatusChangedEventArgs args) + { + ConnectionStatus = GetConnectionStatus(args); + ConnectionStatusType = args.Status switch + { + Core.Connection.ConnectionStatus.Connected => ConnectionStatusType.Ok, + Core.Connection.ConnectionStatus.Connecting => ConnectionStatusType.Connecting, + _ => ConnectionStatusType.Error + }; + } + + + + 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 @@ + + + + + +