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 @@
-
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 @@
-
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 @@
-
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/Connection/ConnectionDisplayNameDialog.xaml.cs b/PettingZoo/UI/Connection/ConnectionDisplayNameDialog.xaml.cs
new file mode 100644
index 0000000..000287e
--- /dev/null
+++ b/PettingZoo/UI/Connection/ConnectionDisplayNameDialog.xaml.cs
@@ -0,0 +1,49 @@
+using System.Linq;
+using System.Windows;
+
+namespace PettingZoo.UI.Connection
+{
+ ///
+ /// Interaction logic for ConnectionDisplayNameDialog.xaml
+ ///
+ public partial class ConnectionDisplayNameDialog
+ {
+ public static bool Execute(ref string displayName)
+ {
+ var viewModel = new ConnectionDisplayNameViewModel
+ {
+ DisplayName = displayName
+ };
+
+
+ var activeWindow = Application.Current.Windows
+ .Cast()
+ .FirstOrDefault(applicationWindow => applicationWindow.IsActive);
+
+ var window = new ConnectionDisplayNameDialog(viewModel)
+ {
+ Owner = activeWindow ?? Application.Current.MainWindow
+ };
+
+ if (!window.ShowDialog().GetValueOrDefault())
+ return false;
+
+ displayName = viewModel.DisplayName;
+ return true;
+ }
+
+
+ public ConnectionDisplayNameDialog(ConnectionDisplayNameViewModel viewModel)
+ {
+ viewModel.OkClick += (_, _) =>
+ {
+ DialogResult = true;
+ };
+
+ DataContext = viewModel;
+ InitializeComponent();
+
+ DisplayNameTextBox.CaretIndex = DisplayNameTextBox.Text.Length;
+ }
+ }
+}
diff --git a/PettingZoo/UI/Connection/ConnectionDisplayNameStrings.Designer.cs b/PettingZoo/UI/Connection/ConnectionDisplayNameStrings.Designer.cs
new file mode 100644
index 0000000..de2828b
--- /dev/null
+++ b/PettingZoo/UI/Connection/ConnectionDisplayNameStrings.Designer.cs
@@ -0,0 +1,90 @@
+//------------------------------------------------------------------------------
+//
+// 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.UI.Connection {
+ 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()]
+ public class ConnectionDisplayNameStrings {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal ConnectionDisplayNameStrings() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.UI.Connection.ConnectionDisplayNameStrings", typeof(ConnectionDisplayNameStrings).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)]
+ public static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Cancel.
+ ///
+ public static string ButtonCancel {
+ get {
+ return ResourceManager.GetString("ButtonCancel", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to OK.
+ ///
+ public static string ButtonOK {
+ get {
+ return ResourceManager.GetString("ButtonOK", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Profile name.
+ ///
+ public static string WindowTitle {
+ get {
+ return ResourceManager.GetString("WindowTitle", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/PettingZoo/UI/Connection/ConnectionDisplayNameStrings.resx b/PettingZoo/UI/Connection/ConnectionDisplayNameStrings.resx
new file mode 100644
index 0000000..bc89e48
--- /dev/null
+++ b/PettingZoo/UI/Connection/ConnectionDisplayNameStrings.resx
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+ Cancel
+
+
+ OK
+
+
+ Profile name
+
+
\ No newline at end of file
diff --git a/PettingZoo/UI/Connection/ConnectionDisplayNameViewModel.cs b/PettingZoo/UI/Connection/ConnectionDisplayNameViewModel.cs
new file mode 100644
index 0000000..1ca01d2
--- /dev/null
+++ b/PettingZoo/UI/Connection/ConnectionDisplayNameViewModel.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Windows.Input;
+
+namespace PettingZoo.UI.Connection
+{
+ public class ConnectionDisplayNameViewModel : BaseViewModel
+ {
+ private string displayName = "";
+
+ private readonly DelegateCommand okCommand;
+
+
+ public string DisplayName
+ {
+ get => displayName;
+ set => SetField(ref displayName, value, delegateCommandsChanged: new [] { okCommand });
+ }
+
+ public ICommand OkCommand => okCommand;
+
+ public event EventHandler? OkClick;
+
+
+ public ConnectionDisplayNameViewModel()
+ {
+ okCommand = new DelegateCommand(OkExecute, OkCanExecute);
+ }
+
+
+ private void OkExecute()
+ {
+ OkClick?.Invoke(this, EventArgs.Empty);
+ }
+
+
+ private bool OkCanExecute()
+ {
+ return !string.IsNullOrWhiteSpace(DisplayName);
+ }
+ }
+}
diff --git a/PettingZoo/UI/Connection/ConnectionViewModel.cs b/PettingZoo/UI/Connection/ConnectionViewModel.cs
new file mode 100644
index 0000000..7d1cf4d
--- /dev/null
+++ b/PettingZoo/UI/Connection/ConnectionViewModel.cs
@@ -0,0 +1,321 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Input;
+using PettingZoo.Core.Settings;
+
+namespace PettingZoo.UI.Connection
+{
+ public class ConnectionViewModel : BaseViewModel
+ {
+ private readonly IConnectionSettingsRepository connectionSettingsRepository;
+ private readonly StoredConnectionSettings defaultSettings;
+ private string host = null!;
+ private string virtualHost = null!;
+ private int port;
+ private string username = null!;
+ private string password = null!;
+
+ private bool subscribe;
+ private string exchange = null!;
+ private string routingKey = null!;
+
+ private bool storePassword;
+
+ private StoredConnectionSettings? selectedStoredConnection;
+
+ private readonly DelegateCommand okCommand;
+ private readonly DelegateCommand saveCommand;
+ private readonly DelegateCommand saveAsCommand;
+ private readonly DelegateCommand deleteCommand;
+
+ private readonly DelegateCommand[] connectionChangedCommands;
+
+
+ public string Host
+ {
+ get => host;
+ set => SetField(ref host, value, delegateCommandsChanged: connectionChangedCommands);
+ }
+
+ public string VirtualHost
+ {
+ get => virtualHost;
+ set => SetField(ref virtualHost, value, delegateCommandsChanged: connectionChangedCommands);
+ }
+
+ public int Port
+ {
+ get => port;
+ set => SetField(ref port, value, delegateCommandsChanged: connectionChangedCommands);
+ }
+
+ public string Username
+ {
+ get => username;
+ set => SetField(ref username, value, delegateCommandsChanged: connectionChangedCommands);
+ }
+
+ public string Password
+ {
+ get => password;
+ set => SetField(ref password, value, delegateCommandsChanged: connectionChangedCommands);
+ }
+
+
+ public bool Subscribe
+ {
+ get => subscribe;
+ set => SetField(ref subscribe, value, delegateCommandsChanged: connectionChangedCommands);
+ }
+
+ public string Exchange
+ {
+ get => exchange;
+ set
+ {
+ if (SetField(ref exchange, value, delegateCommandsChanged: connectionChangedCommands))
+ AutoToggleSubscribe();
+ }
+ }
+
+ public string RoutingKey
+ {
+ get => routingKey;
+ set
+ {
+ if (SetField(ref routingKey, value, delegateCommandsChanged: connectionChangedCommands))
+ AutoToggleSubscribe();
+ }
+ }
+
+
+ public bool StorePassword
+ {
+ get => storePassword;
+ set => SetField(ref storePassword, value, delegateCommandsChanged: connectionChangedCommands);
+ }
+
+
+ public ObservableCollection StoredConnections { get; } = new();
+
+ public StoredConnectionSettings? SelectedStoredConnection
+ {
+ get => selectedStoredConnection;
+ set
+ {
+ if (value == null)
+ return;
+
+ if (!SetField(ref selectedStoredConnection, value, delegateCommandsChanged: new [] { deleteCommand }))
+ return;
+
+ DisableCommandsChanged(
+ () =>
+ {
+ Host = value.Host;
+ VirtualHost = value.VirtualHost;
+ Port = value.Port;
+ Username = value.Username;
+ Password = value.Password;
+ StorePassword = value.StorePassword;
+
+ Exchange = value.Exchange;
+ RoutingKey = value.RoutingKey;
+ Subscribe = value.Subscribe;
+ },
+ connectionChangedCommands);
+ }
+ }
+
+
+ public ICommand OkCommand => okCommand;
+ public ICommand SaveCommand => saveCommand;
+ public ICommand SaveAsCommand => saveAsCommand;
+ public ICommand DeleteCommand => deleteCommand;
+
+ public event EventHandler? OkClick;
+
+
+ public ConnectionViewModel(IConnectionSettingsRepository connectionSettingsRepository, StoredConnectionSettings defaultSettings)
+ {
+ this.connectionSettingsRepository = connectionSettingsRepository;
+ this.defaultSettings = defaultSettings;
+
+ okCommand = new DelegateCommand(OkExecute, OkCanExecute);
+ saveCommand = new DelegateCommand(SaveExecute, SaveCanExecute);
+ saveAsCommand = new DelegateCommand(SaveAsExecute, SaveAsCanExecute);
+ deleteCommand = new DelegateCommand(DeleteExecute, DeleteCanExecute);
+
+ connectionChangedCommands = new[] { saveCommand, saveAsCommand, okCommand };
+ }
+
+
+ public async Task Initialize()
+ {
+ var defaultConnection = new StoredConnectionSettings(
+ Guid.Empty,
+ ConnectionWindowStrings.LastUsedDisplayName,
+ defaultSettings.StorePassword,
+ defaultSettings.Host,
+ defaultSettings.VirtualHost,
+ defaultSettings.Port,
+ defaultSettings.Username,
+ defaultSettings.Password,
+ defaultSettings.Subscribe,
+ defaultSettings.Exchange,
+ defaultSettings.RoutingKey);
+
+ var isStored = false;
+
+ foreach (var storedConnectionSettings in await connectionSettingsRepository.GetStored())
+ {
+ if (!isStored && storedConnectionSettings.SameParameters(defaultConnection, false))
+ {
+ SelectedStoredConnection = storedConnectionSettings;
+ isStored = true;
+ }
+
+ StoredConnections.Add(storedConnectionSettings);
+ }
+
+ if (isStored)
+ {
+ // The last used parameters match a stored connection, insert the "New connection" item with default parameters
+ StoredConnections.Insert(0, new StoredConnectionSettings(Guid.Empty, ConnectionWindowStrings.LastUsedDisplayName, true, ConnectionSettings.Default));
+ }
+ else
+ {
+ // No match, use the passed parameters
+ StoredConnections.Insert(0, defaultConnection);
+ SelectedStoredConnection = defaultConnection;
+ }
+ }
+
+
+ public ConnectionSettings ToModel()
+ {
+ return new ConnectionSettings(Host, VirtualHost, Port, Username, Password, Subscribe, Exchange, RoutingKey);
+ }
+
+
+ private bool ValidConnection(bool requirePassword)
+ {
+ return !string.IsNullOrWhiteSpace(Host) &&
+ !string.IsNullOrWhiteSpace(VirtualHost) &&
+ Port > 0 &&
+ !string.IsNullOrWhiteSpace(Username) &&
+ (!requirePassword || !string.IsNullOrWhiteSpace(Password)) &&
+ (!Subscribe || (
+ !string.IsNullOrWhiteSpace(Exchange) &&
+ !string.IsNullOrWhiteSpace(RoutingKey)
+ ));
+ }
+
+
+ private void AutoToggleSubscribe()
+ {
+ Subscribe = !string.IsNullOrWhiteSpace(Exchange) && !string.IsNullOrWhiteSpace(RoutingKey);
+ }
+
+
+ private void OkExecute()
+ {
+ OkClick?.Invoke(this, EventArgs.Empty);
+ }
+
+
+ private bool OkCanExecute()
+ {
+ return ValidConnection(true);
+ }
+
+
+ private async void SaveExecute()
+ {
+ if (SelectedStoredConnection == null || SelectedStoredConnection.Id == Guid.Empty)
+ return;
+
+ var selectedIndex = StoredConnections.IndexOf(SelectedStoredConnection);
+
+ var updatedStoredConnection = await connectionSettingsRepository.Update(SelectedStoredConnection.Id, SelectedStoredConnection.DisplayName, StorePassword, ToModel());
+
+
+ StoredConnections[selectedIndex] = updatedStoredConnection;
+ SelectedStoredConnection = updatedStoredConnection;
+ }
+
+
+ private bool SaveCanExecute()
+ {
+ return SelectedStoredConnection != null &&
+ SelectedStoredConnection.Id != Guid.Empty &&
+ ValidConnection(false) &&
+ (
+ !ToModel().SameParameters(SelectedStoredConnection, StorePassword) ||
+ SelectedStoredConnection.StorePassword != StorePassword
+ );
+ }
+
+
+ private async void SaveAsExecute()
+ {
+ // TODO create and enforce unique name?
+ var displayName = SelectedStoredConnection != null && SelectedStoredConnection.Id != Guid.Empty ? SelectedStoredConnection.DisplayName : "";
+
+ if (!ConnectionDisplayNameDialog.Execute(ref displayName))
+ return;
+
+ var storedConnectionSettings = await connectionSettingsRepository.Add(displayName, StorePassword, ToModel());
+
+ StoredConnections.Add(storedConnectionSettings);
+ SelectedStoredConnection = storedConnectionSettings;
+ }
+
+
+ private bool SaveAsCanExecute()
+ {
+ return ValidConnection(false);
+ }
+
+
+ private async void DeleteExecute()
+ {
+ if (SelectedStoredConnection == null || SelectedStoredConnection.Id == Guid.Empty)
+ return;
+
+ var selectedIndex = StoredConnections.IndexOf(SelectedStoredConnection);
+
+ if (MessageBox.Show(
+ string.Format(ConnectionWindowStrings.DeleteConfirm, SelectedStoredConnection.DisplayName),
+ ConnectionWindowStrings.DeleteConfirmTitle,
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Question) != MessageBoxResult.Yes)
+ return;
+
+ await connectionSettingsRepository.Delete(SelectedStoredConnection.Id);
+
+ StoredConnections.Remove(SelectedStoredConnection);
+ if (selectedIndex >= StoredConnections.Count)
+ selectedIndex--;
+
+ SelectedStoredConnection = StoredConnections[selectedIndex];
+ }
+
+
+ private bool DeleteCanExecute()
+ {
+ return SelectedStoredConnection != null && SelectedStoredConnection.Id != Guid.Empty;
+ }
+ }
+
+
+ public class DesignTimeConnectionViewModel : ConnectionViewModel
+ {
+ public DesignTimeConnectionViewModel() : base(null!, null!)
+ {
+ StoredConnections.Add(new StoredConnectionSettings(Guid.Empty, "Dummy", true, ConnectionSettings.Default));
+ }
+ }
+}
diff --git a/PettingZoo/UI/Connection/ConnectionWindow.xaml b/PettingZoo/UI/Connection/ConnectionWindow.xaml
new file mode 100644
index 0000000..e6a6ee3
--- /dev/null
+++ b/PettingZoo/UI/Connection/ConnectionWindow.xaml
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PettingZoo/UI/Connection/ConnectionWindow.xaml.cs b/PettingZoo/UI/Connection/ConnectionWindow.xaml.cs
new file mode 100644
index 0000000..f58c2fa
--- /dev/null
+++ b/PettingZoo/UI/Connection/ConnectionWindow.xaml.cs
@@ -0,0 +1,72 @@
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using PettingZoo.Core.Settings;
+
+namespace PettingZoo.UI.Connection
+{
+ public class WindowConnectionDialog : IConnectionDialog
+ {
+ private readonly IConnectionSettingsRepository connectionSettingsRepository;
+
+ public WindowConnectionDialog(IConnectionSettingsRepository connectionSettingsRepository)
+ {
+ this.connectionSettingsRepository = connectionSettingsRepository;
+ }
+
+
+ public async Task Show()
+ {
+ var lastUsed = await connectionSettingsRepository.GetLastUsed();
+
+ var viewModel = new ConnectionViewModel(connectionSettingsRepository, lastUsed);
+ await viewModel.Initialize();
+
+ var window = new ConnectionWindow(viewModel)
+ {
+ Owner = Application.Current.MainWindow
+ };
+
+ viewModel.OkClick += (_, _) =>
+ {
+ window.DialogResult = true;
+ };
+
+
+ if (!window.ShowDialog().GetValueOrDefault())
+ return null;
+
+ var newSettings = viewModel.ToModel();
+ await connectionSettingsRepository.StoreLastUsed(viewModel.StorePassword, newSettings);
+
+ return newSettings;
+ }
+ }
+
+
+ public partial class ConnectionWindow
+ {
+ public ConnectionWindow(ConnectionViewModel viewModel)
+ {
+ DataContext = viewModel;
+ InitializeComponent();
+ }
+
+
+ private void NumericPreviewTextInput(object sender, TextCompositionEventArgs args)
+ {
+ if (!char.IsDigit(args.Text, args.Text.Length - 1))
+ args.Handled = true;
+ }
+
+
+ private void CaretToEnd(object sender, RoutedEventArgs e)
+ {
+ if (sender is not TextBox textBox)
+ return;
+
+ textBox.CaretIndex = textBox.Text.Length;
+ }
+ }
+}
diff --git a/PettingZoo/UI/Connection/ConnectionWindowStrings.Designer.cs b/PettingZoo/UI/Connection/ConnectionWindowStrings.Designer.cs
new file mode 100644
index 0000000..2101f00
--- /dev/null
+++ b/PettingZoo/UI/Connection/ConnectionWindowStrings.Designer.cs
@@ -0,0 +1,225 @@
+//------------------------------------------------------------------------------
+//
+// 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.UI.Connection {
+ 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()]
+ public class ConnectionWindowStrings {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal ConnectionWindowStrings() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.UI.Connection.ConnectionWindowStrings", typeof(ConnectionWindowStrings).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)]
+ public static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Cancel.
+ ///
+ public static string ButtonCancel {
+ get {
+ return ResourceManager.GetString("ButtonCancel", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Delete.
+ ///
+ public static string ButtonDelete {
+ get {
+ return ResourceManager.GetString("ButtonDelete", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Connect.
+ ///
+ public static string ButtonOK {
+ get {
+ return ResourceManager.GetString("ButtonOK", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Save.
+ ///
+ public static string ButtonSave {
+ get {
+ return ResourceManager.GetString("ButtonSave", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Save as....
+ ///
+ public static string ButtonSaveAs {
+ get {
+ return ResourceManager.GetString("ButtonSaveAs", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Do you want to delete the connection settings "{0}"?.
+ ///
+ public static string DeleteConfirm {
+ get {
+ return ResourceManager.GetString("DeleteConfirm", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Delete connection.
+ ///
+ public static string DeleteConfirmTitle {
+ get {
+ return ResourceManager.GetString("DeleteConfirmTitle", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Exchange.
+ ///
+ public static string LabelExchange {
+ get {
+ return ResourceManager.GetString("LabelExchange", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Host.
+ ///
+ public static string LabelHost {
+ get {
+ return ResourceManager.GetString("LabelHost", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Password.
+ ///
+ public static string LabelPassword {
+ get {
+ return ResourceManager.GetString("LabelPassword", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Port.
+ ///
+ public static string LabelPort {
+ get {
+ return ResourceManager.GetString("LabelPort", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Routing key.
+ ///
+ public static string LabelRoutingKey {
+ get {
+ return ResourceManager.GetString("LabelRoutingKey", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Store password.
+ ///
+ public static string LabelStorePassword {
+ get {
+ return ResourceManager.GetString("LabelStorePassword", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Subscribe.
+ ///
+ public static string LabelSubscribe {
+ get {
+ return ResourceManager.GetString("LabelSubscribe", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Username.
+ ///
+ public static string LabelUsername {
+ get {
+ return ResourceManager.GetString("LabelUsername", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Virtual host.
+ ///
+ public static string LabelVirtualHost {
+ get {
+ return ResourceManager.GetString("LabelVirtualHost", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to <New connection>.
+ ///
+ public static string LastUsedDisplayName {
+ get {
+ return ResourceManager.GetString("LastUsedDisplayName", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Connection parameters.
+ ///
+ public static string WindowTitle {
+ get {
+ return ResourceManager.GetString("WindowTitle", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/PettingZoo/UI/Connection/ConnectionWindowStrings.resx b/PettingZoo/UI/Connection/ConnectionWindowStrings.resx
new file mode 100644
index 0000000..5b739e5
--- /dev/null
+++ b/PettingZoo/UI/Connection/ConnectionWindowStrings.resx
@@ -0,0 +1,174 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+ Cancel
+
+
+ Delete
+
+
+ Connect
+
+
+ Save
+
+
+ Save as...
+
+
+ Do you want to delete the connection settings "{0}"?
+
+
+ Delete connection
+
+
+ Exchange
+
+
+ Host
+
+
+ Password
+
+
+ Port
+
+
+ Routing key
+
+
+ Store password
+
+
+ Subscribe
+
+
+ Username
+
+
+ Virtual host
+
+
+ <New connection>
+
+
+ Connection parameters
+
+
\ No newline at end of file
diff --git a/PettingZoo/UI/Connection/IConnectionDialog.cs b/PettingZoo/UI/Connection/IConnectionDialog.cs
new file mode 100644
index 0000000..7b52677
--- /dev/null
+++ b/PettingZoo/UI/Connection/IConnectionDialog.cs
@@ -0,0 +1,11 @@
+using System;
+using System.Threading.Tasks;
+using PettingZoo.Core.Settings;
+
+namespace PettingZoo.UI.Connection
+{
+ public interface IConnectionDialog
+ {
+ Task Show();
+ }
+}
diff --git a/PettingZoo/UI/DelegateCommand.cs b/PettingZoo/UI/DelegateCommand.cs
new file mode 100644
index 0000000..8404c02
--- /dev/null
+++ b/PettingZoo/UI/DelegateCommand.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Windows.Input;
+
+namespace PettingZoo.UI
+{
+ public class DelegateCommand : ICommand where T : class?
+ {
+ 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()
+ {
+ CanExecuteChanged?.Invoke(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()
+ {
+ CanExecuteChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+}
diff --git a/PettingZoo/UI/DependencyObjectExtensions.cs b/PettingZoo/UI/DependencyObjectExtensions.cs
new file mode 100644
index 0000000..fea8c66
--- /dev/null
+++ b/PettingZoo/UI/DependencyObjectExtensions.cs
@@ -0,0 +1,29 @@
+using System;
+using System.ComponentModel;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Windows;
+
+namespace PettingZoo.UI
+{
+ public static class DependencyObjectExtensions
+ {
+ public static IObservable OnPropertyChanges(this DependencyObject source, DependencyProperty property)
+ {
+ return Observable.Create(o =>
+ {
+ var dpd = DependencyPropertyDescriptor.FromProperty(property, property.OwnerType);
+ if (dpd == null)
+ o.OnError(new InvalidOperationException("Can not register change handler for this dependency property."));
+
+ void Handler(object? sender, EventArgs e)
+ {
+ o.OnNext((T)source.GetValue(property));
+ }
+
+ dpd?.AddValueChanged(source, Handler);
+ return Disposable.Create(() => dpd?.RemoveValueChanged(source, Handler));
+ });
+ }
+ }
+}
diff --git a/PettingZoo/UI/EnumBooleanConverter.cs b/PettingZoo/UI/EnumBooleanConverter.cs
new file mode 100644
index 0000000..be439c2
--- /dev/null
+++ b/PettingZoo/UI/EnumBooleanConverter.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace PettingZoo.UI
+{
+ public class EnumBooleanConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value.Equals(parameter);
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return ((bool)value) ? parameter : Binding.DoNothing;
+ }
+ }
+}
diff --git a/PettingZoo/UI/Example/ExamplePickerDialog.xaml b/PettingZoo/UI/Example/ExamplePickerDialog.xaml
new file mode 100644
index 0000000..3b3684a
--- /dev/null
+++ b/PettingZoo/UI/Example/ExamplePickerDialog.xaml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/PettingZoo/UI/Example/ExamplePickerDialog.xaml.cs b/PettingZoo/UI/Example/ExamplePickerDialog.xaml.cs
new file mode 100644
index 0000000..0ec7066
--- /dev/null
+++ b/PettingZoo/UI/Example/ExamplePickerDialog.xaml.cs
@@ -0,0 +1,15 @@
+using System.Windows;
+
+namespace PettingZoo.UI.Example
+{
+ ///
+ /// Interaction logic for ExamplePickerDialog.xaml
+ ///
+ public partial class ExamplePickerDialog : Window
+ {
+ public ExamplePickerDialog()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/PettingZoo/UI/Example/ExamplePickerDialogStrings.Designer.cs b/PettingZoo/UI/Example/ExamplePickerDialogStrings.Designer.cs
new file mode 100644
index 0000000..5f39c62
--- /dev/null
+++ b/PettingZoo/UI/Example/ExamplePickerDialogStrings.Designer.cs
@@ -0,0 +1,72 @@
+//------------------------------------------------------------------------------
+//
+// 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.UI.Example {
+ 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()]
+ public class ExamplePickerDialogStrings {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal ExamplePickerDialogStrings() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.UI.Example.ExamplePickerDialogStrings", typeof(ExamplePickerDialogStrings).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)]
+ public static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Select example.
+ ///
+ public static string WindowTitle {
+ get {
+ return ResourceManager.GetString("WindowTitle", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/PettingZoo/UI/Example/ExamplePickerDialogStrings.resx b/PettingZoo/UI/Example/ExamplePickerDialogStrings.resx
new file mode 100644
index 0000000..a0b7311
--- /dev/null
+++ b/PettingZoo/UI/Example/ExamplePickerDialogStrings.resx
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+ Select example
+
+
\ No newline at end of file
diff --git a/PettingZoo/UI/Example/ExamplePickerDialogViewModel.cs b/PettingZoo/UI/Example/ExamplePickerDialogViewModel.cs
new file mode 100644
index 0000000..35dd85f
--- /dev/null
+++ b/PettingZoo/UI/Example/ExamplePickerDialogViewModel.cs
@@ -0,0 +1,6 @@
+namespace PettingZoo.UI.Example
+{
+ public class ExamplePickerDialogViewModel
+ {
+ }
+}
diff --git a/Infrastructure/GridLayout.cs b/PettingZoo/UI/GridLayout.cs
similarity index 67%
rename from Infrastructure/GridLayout.cs
rename to PettingZoo/UI/GridLayout.cs
index d4ed843..a1f577f 100644
--- a/Infrastructure/GridLayout.cs
+++ b/PettingZoo/UI/GridLayout.cs
@@ -1,7 +1,7 @@
using System.Windows;
using System.Windows.Controls;
-namespace PettingZoo.Infrastructure
+namespace PettingZoo.UI
{
// Source: http://daniel-albuschat.blogspot.nl/2011/07/gridlayout-for-wpf-escape-margin-hell.html
@@ -32,7 +32,7 @@ namespace PettingZoo.Infrastructure
public Thickness ChildMargin
{
- get { return (Thickness) GetValue(ChildMarginProperty); }
+ get => (Thickness) GetValue(ChildMarginProperty);
set
{
SetValue(ChildMarginProperty, value);
@@ -45,12 +45,13 @@ namespace PettingZoo.Infrastructure
public void UpdateChildMargins()
{
- int maxColumn = 0;
- int maxRow = 0;
+ var maxColumn = 0;
+ var maxRow = 0;
foreach (UIElement element in InternalChildren)
{
- int row = GetRow(element);
- int column = GetColumn(element);
+ var row = GetRow(element);
+ var column = GetColumn(element);
+
if (row > maxRow)
maxRow = row;
if (column > maxColumn)
@@ -58,32 +59,31 @@ namespace PettingZoo.Infrastructure
}
foreach (UIElement element in InternalChildren)
{
- var fe = element as FrameworkElement;
- if (null != fe)
- {
- int row = GetRow(fe);
- int column = GetColumn(fe);
- double factorLeft = 0.5;
- double factorTop = 0.5;
- double factorRight = 0.5;
- double factorBottom = 0.5;
- // Top row - no top margin
- if (row == 0)
- factorTop = 0;
- // Bottom row - no bottom margin
- if (row == maxRow)
- factorBottom = 0;
- // Leftmost column = no left margin
- if (column == 0)
- factorLeft = 0;
- // Rightmost column - no right margin
- if (column == maxColumn)
- factorRight = 0;
- fe.Margin = new Thickness(ChildMargin.Left*factorLeft,
- ChildMargin.Top*factorTop,
- ChildMargin.Right*factorRight,
- ChildMargin.Bottom*factorBottom);
- }
+ if (element is not FrameworkElement fe)
+ continue;
+
+ var row = GetRow(fe);
+ var column = GetColumn(fe);
+ var factorLeft = 0.5;
+ var factorTop = 0.5;
+ var factorRight = 0.5;
+ var factorBottom = 0.5;
+ // Top row - no top margin
+ if (row == 0)
+ factorTop = 0;
+ // Bottom row - no bottom margin
+ if (row == maxRow)
+ factorBottom = 0;
+ // Leftmost column = no left margin
+ if (column == 0)
+ factorLeft = 0;
+ // Rightmost column - no right margin
+ if (column == maxColumn)
+ factorRight = 0;
+ fe.Margin = new Thickness(ChildMargin.Left*factorLeft,
+ ChildMargin.Top*factorTop,
+ ChildMargin.Right*factorRight,
+ ChildMargin.Bottom*factorBottom);
}
}
diff --git a/Infrastructure/ListBoxAutoScroll.cs b/PettingZoo/UI/ListBoxAutoScroll.cs
similarity index 75%
rename from Infrastructure/ListBoxAutoScroll.cs
rename to PettingZoo/UI/ListBoxAutoScroll.cs
index 4d2415c..b378e3b 100644
--- a/Infrastructure/ListBoxAutoScroll.cs
+++ b/PettingZoo/UI/ListBoxAutoScroll.cs
@@ -2,11 +2,11 @@
using System.Collections;
using System.Collections.Specialized;
using System.Windows;
-using System.Windows.Data;
using System.Windows.Controls;
+using System.Windows.Data;
using System.Windows.Media;
-namespace PettingZoo.Infrastructure
+namespace PettingZoo.UI
{
// Source: https://social.msdn.microsoft.com/Forums/vstudio/en-US/0f524459-b14e-4f9a-8264-267953418a2d/trivial-listboxlistview-autoscroll?forum=wpf
//
@@ -29,7 +29,7 @@ namespace PettingZoo.Infrastructure
public static void SetAutoScroll(System.Windows.Controls.ListBox instance, bool value)
{
- var oldHandler = (AutoScrollHandler) instance.GetValue(AutoScrollHandlerProperty);
+ var oldHandler = (AutoScrollHandler?)instance.GetValue(AutoScrollHandlerProperty);
if (oldHandler != null)
{
oldHandler.Dispose();
@@ -51,7 +51,7 @@ namespace PettingZoo.Infrastructure
ItemsSourcePropertyChanged));
private readonly System.Windows.Controls.ListBox target;
- private ScrollViewer scrollViewer;
+ private ScrollViewer? scrollViewer;
public AutoScrollHandler(System.Windows.Controls.ListBox target)
{
@@ -74,8 +74,8 @@ namespace PettingZoo.Infrastructure
public IEnumerable ItemsSource
{
- get { return (IEnumerable) GetValue(ItemsSourceProperty); }
- set { SetValue(ItemsSourceProperty, value); }
+ get => (IEnumerable) GetValue(ItemsSourceProperty);
+ set => SetValue(ItemsSourceProperty, value);
}
@@ -87,16 +87,14 @@ namespace PettingZoo.Infrastructure
private void ItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
- var collection = oldValue as INotifyCollectionChanged;
- if (collection != null)
- collection.CollectionChanged -= Collection_CollectionChanged;
+ if (oldValue is INotifyCollectionChanged oldCollection)
+ oldCollection.CollectionChanged -= Collection_CollectionChanged;
- collection = newValue as INotifyCollectionChanged;
- if (collection != null)
- collection.CollectionChanged += Collection_CollectionChanged;
+ if (newValue is INotifyCollectionChanged newCollection)
+ newCollection.CollectionChanged += Collection_CollectionChanged;
}
- private void Collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ private void Collection_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action != NotifyCollectionChangedAction.Add || e.NewItems == null || e.NewItems.Count < 1)
return;
@@ -109,24 +107,31 @@ namespace PettingZoo.Infrastructure
return;
}
- if (Math.Abs(scrollViewer.VerticalOffset - scrollViewer.ScrollableHeight) < 1)
- target.ScrollIntoView(e.NewItems[e.NewItems.Count - 1]);
+ if (e.NewItems.Count == 0)
+ return;
+
+ // If not already at the bottom, keep the position stable
+ if (Math.Abs(scrollViewer.VerticalOffset - scrollViewer.ScrollableHeight) > 0)
+ return;
+
+ var item = e.NewItems[^1];
+ if (item != null)
+ target.ScrollIntoView(item);
}
- private static ScrollViewer FindScrollViewer(DependencyObject parent)
+ private static ScrollViewer? FindScrollViewer(DependencyObject parent)
{
var childCount = VisualTreeHelper.GetChildrenCount(parent);
for (var childIndex = 0; childIndex < childCount; childIndex++)
{
var child = VisualTreeHelper.GetChild(parent, childIndex);
- var scrollViewer = (child as ScrollViewer);
- if (scrollViewer != null)
+ if (child is ScrollViewer scrollViewer)
return scrollViewer;
- scrollViewer = FindScrollViewer(child);
- if (scrollViewer != null)
- return scrollViewer;
+ var childScrollViewer = FindScrollViewer(child);
+ if (childScrollViewer != null)
+ return childScrollViewer;
}
return null;
diff --git a/PettingZoo/UI/Main/ITabContainer.cs b/PettingZoo/UI/Main/ITabContainer.cs
new file mode 100644
index 0000000..4591fda
--- /dev/null
+++ b/PettingZoo/UI/Main/ITabContainer.cs
@@ -0,0 +1,8 @@
+namespace PettingZoo.UI.Main
+{
+ public interface ITabContainer
+ {
+ public double TabWidth { get; }
+ public double TabHeight { get; }
+ }
+}
diff --git a/PettingZoo/UI/Main/MainWindow.xaml b/PettingZoo/UI/Main/MainWindow.xaml
new file mode 100644
index 0000000..a5898da
--- /dev/null
+++ b/PettingZoo/UI/Main/MainWindow.xaml
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PettingZoo/UI/Subscribe/SubscribeWindow.xaml.cs b/PettingZoo/UI/Subscribe/SubscribeWindow.xaml.cs
new file mode 100644
index 0000000..5937224
--- /dev/null
+++ b/PettingZoo/UI/Subscribe/SubscribeWindow.xaml.cs
@@ -0,0 +1,37 @@
+using System.Windows;
+
+namespace PettingZoo.UI.Subscribe
+{
+ public class WindowSubscribeDialog : ISubscribeDialog
+ {
+ public SubscribeDialogParams? Show(SubscribeDialogParams? defaultParams = null)
+ {
+ var viewModel = new SubscribeViewModel(defaultParams ?? SubscribeDialogParams.Default);
+ var window = new SubscribeWindow(viewModel)
+ {
+ Owner = Application.Current.MainWindow
+ };
+
+ viewModel.OkClick += (_, _) =>
+ {
+ window.DialogResult = true;
+ };
+
+ return window.ShowDialog().GetValueOrDefault()
+ ? viewModel.ToModel()
+ : null;
+ }
+ }
+
+
+ public partial class SubscribeWindow
+ {
+ public SubscribeWindow(SubscribeViewModel viewModel)
+ {
+ WindowStartupLocation = WindowStartupLocation.CenterOwner;
+
+ DataContext = viewModel;
+ InitializeComponent();
+ }
+ }
+}
diff --git a/PettingZoo/UI/Subscribe/SubscribeWindowStrings.Designer.cs b/PettingZoo/UI/Subscribe/SubscribeWindowStrings.Designer.cs
new file mode 100644
index 0000000..8ed6465
--- /dev/null
+++ b/PettingZoo/UI/Subscribe/SubscribeWindowStrings.Designer.cs
@@ -0,0 +1,108 @@
+//------------------------------------------------------------------------------
+//
+// 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.UI.Subscribe {
+ 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", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ public class SubscribeWindowStrings {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal SubscribeWindowStrings() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.UI.Subscribe.SubscribeWindowStrings", typeof(SubscribeWindowStrings).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)]
+ public static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Cancel.
+ ///
+ public static string ButtonCancel {
+ get {
+ return ResourceManager.GetString("ButtonCancel", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to OK.
+ ///
+ public static string ButtonOK {
+ get {
+ return ResourceManager.GetString("ButtonOK", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Exchange:.
+ ///
+ public static string LabelExchange {
+ get {
+ return ResourceManager.GetString("LabelExchange", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Routing key:.
+ ///
+ public static string LabelRoutingKey {
+ get {
+ return ResourceManager.GetString("LabelRoutingKey", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Subscribe parameters.
+ ///
+ public static string WindowTitle {
+ get {
+ return ResourceManager.GetString("WindowTitle", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/PettingZoo/UI/Subscribe/SubscribeWindowStrings.resx b/PettingZoo/UI/Subscribe/SubscribeWindowStrings.resx
new file mode 100644
index 0000000..e0c4346
--- /dev/null
+++ b/PettingZoo/UI/Subscribe/SubscribeWindowStrings.resx
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+ Cancel
+
+
+ OK
+
+
+ Exchange:
+
+
+ Routing key:
+
+
+ Subscribe parameters
+
+
\ No newline at end of file
diff --git a/PettingZoo/UI/SvgIconHelper.cs b/PettingZoo/UI/SvgIconHelper.cs
new file mode 100644
index 0000000..3948dde
--- /dev/null
+++ b/PettingZoo/UI/SvgIconHelper.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Windows;
+using System.Windows.Media;
+using SharpVectors.Dom.Svg;
+using SharpVectors.Renderers.Utils;
+using SharpVectors.Renderers.Wpf;
+
+namespace PettingZoo.UI
+{
+ public static class SvgIconHelper
+ {
+ public static ImageSource LoadFromResource(string resourceName)
+ {
+ var streamInfo = Application.GetResourceStream(new Uri($"/PettingZoo;component{resourceName}", UriKind.Relative));
+ if (streamInfo == null)
+ throw new ArgumentException($"Resource '{resourceName}' not found");
+
+ // Basically the code used in FileSvgConverter, but that only supports outputting to a file not returning the Drawing
+ var wpfDrawingSettings = new WpfDrawingSettings
+ {
+ IncludeRuntime = true,
+ TextAsGeometry = true
+ };
+ var wpfRenderer = new WpfDrawingRenderer(wpfDrawingSettings);
+ var wpfWindow = new WpfSvgWindow(0, 0, wpfRenderer);
+
+ wpfWindow.LoadDocument(streamInfo.Stream, wpfDrawingSettings);
+ wpfRenderer.InvalidRect = SvgRectF.Empty;
+ wpfRenderer.Render(wpfWindow.Document);
+
+ if (wpfRenderer.Drawing == null)
+ throw new ArgumentException($"Resource '{resourceName}' is not a valid SVG");
+
+ return new DrawingImage(wpfRenderer.Drawing);
+ }
+ }
+}
diff --git a/PettingZoo/UI/Tab/ITab.cs b/PettingZoo/UI/Tab/ITab.cs
new file mode 100644
index 0000000..2448b1f
--- /dev/null
+++ b/PettingZoo/UI/Tab/ITab.cs
@@ -0,0 +1,43 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
+
+namespace PettingZoo.UI.Tab
+{
+ public interface ITabToolbarCommands : INotifyPropertyChanged
+ {
+ IEnumerable ToolbarCommands { get; }
+ }
+
+
+ public interface ITabActivate
+ {
+ void Activate();
+ void Deactivate();
+ }
+
+
+ public interface ITab : ITabToolbarCommands, ITabActivate
+ {
+ string Title { get; }
+ ContentControl Content { get; }
+ }
+
+
+ public readonly struct TabToolbarCommand
+ {
+ public ICommand Command { get; }
+ public string Caption { get; }
+ public ImageSource Icon { get; }
+
+
+ public TabToolbarCommand(ICommand command, string caption, ImageSource icon)
+ {
+ Command = command;
+ Caption = caption;
+ Icon = icon;
+ }
+ }
+}
diff --git a/PettingZoo/UI/Tab/ITabFactory.cs b/PettingZoo/UI/Tab/ITabFactory.cs
new file mode 100644
index 0000000..e7f5ac3
--- /dev/null
+++ b/PettingZoo/UI/Tab/ITabFactory.cs
@@ -0,0 +1,14 @@
+using PettingZoo.Core.Connection;
+
+namespace PettingZoo.UI.Tab
+{
+ // Passing the closeTabCommand is necessary because I haven't figured out how to bind the main window's
+ // context menu items for the tab to the main window's datacontext yet. RelativeSource doesn't seem to work
+ // because the popup is it's own window. Refactor if a better solution is found.
+
+ public interface ITabFactory
+ {
+ ITab CreateSubscriberTab(IConnection connection, ISubscriber subscriber);
+ ITab CreatePublisherTab(IConnection connection, ReceivedMessageInfo? fromReceivedMessage = null);
+ }
+}
diff --git a/PettingZoo/UI/Tab/ITabHost.cs b/PettingZoo/UI/Tab/ITabHost.cs
new file mode 100644
index 0000000..e73ec07
--- /dev/null
+++ b/PettingZoo/UI/Tab/ITabHost.cs
@@ -0,0 +1,10 @@
+namespace PettingZoo.UI.Tab
+{
+ public interface ITabHost
+ {
+ void AddTab(ITab tab);
+
+ void DockTab(ITab tab);
+ void UndockedTabClosed(ITab tab);
+ }
+}
diff --git a/PettingZoo/UI/Tab/Publisher/IPublishDestination.cs b/PettingZoo/UI/Tab/Publisher/IPublishDestination.cs
new file mode 100644
index 0000000..d4f5788
--- /dev/null
+++ b/PettingZoo/UI/Tab/Publisher/IPublishDestination.cs
@@ -0,0 +1,10 @@
+namespace PettingZoo.UI.Tab.Publisher
+{
+ public interface IPublishDestination
+ {
+ string Exchange { get; }
+ string RoutingKey { get; }
+
+ string? GetReplyTo();
+ }
+}
diff --git a/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml b/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml
new file mode 100644
index 0000000..9e45427
--- /dev/null
+++ b/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml.cs b/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml.cs
new file mode 100644
index 0000000..8fd4b99
--- /dev/null
+++ b/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml.cs
@@ -0,0 +1,141 @@
+using System;
+using System.Reactive.Linq;
+using System.Threading;
+using System.Windows;
+using System.Windows.Data;
+
+namespace PettingZoo.UI.Tab.Publisher
+{
+ ///
+ /// Interaction logic for PayloadEditorControl.xaml
+ ///
+ public partial class PayloadEditorControl
+ {
+ private readonly PayloadEditorViewModel viewModel = new();
+
+
+ public static readonly DependencyProperty ContentTypeProperty
+ = DependencyProperty.Register(
+ "ContentType",
+ typeof(string),
+ typeof(PayloadEditorControl),
+ new FrameworkPropertyMetadata("")
+ {
+ BindsTwoWayByDefault = true,
+ DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
+ }
+ );
+
+ public string ContentType
+ {
+ get => viewModel.ContentType;
+ set
+ {
+ if (value == viewModel.ContentType)
+ return;
+
+ SetValue(ContentTypeProperty, value);
+ viewModel.ContentType = value;
+ }
+ }
+
+
+ public static readonly DependencyProperty FixedJsonProperty
+ = DependencyProperty.Register(
+ "FixedJson",
+ typeof(bool),
+ typeof(PayloadEditorControl),
+ new PropertyMetadata(false)
+ );
+
+ public bool FixedJson
+ {
+ get => viewModel.FixedJson;
+ set
+ {
+ if (value == viewModel.FixedJson)
+ return;
+
+ SetValue(FixedJsonProperty, value);
+ viewModel.FixedJson = value;
+ }
+ }
+
+ public static readonly DependencyProperty PayloadProperty
+ = DependencyProperty.Register(
+ "Payload",
+ typeof(string),
+ typeof(PayloadEditorControl),
+ new FrameworkPropertyMetadata("")
+ {
+ BindsTwoWayByDefault = true,
+ DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
+ }
+ );
+
+ public string Payload
+ {
+ get => viewModel.Payload;
+ set
+ {
+ if (value == viewModel.Payload)
+ return;
+
+ SetValue(PayloadProperty, value);
+ viewModel.Payload = value;
+ }
+ }
+
+ public PayloadEditorControl()
+ {
+ // Keep the exposed properties in sync with the ViewModel
+ this.OnPropertyChanges(ContentTypeProperty)
+ .ObserveOn(SynchronizationContext.Current!)
+ .Subscribe(value =>
+ {
+ viewModel.ContentType = value;
+ });
+
+
+ this.OnPropertyChanges(FixedJsonProperty)
+ .ObserveOn(SynchronizationContext.Current!)
+ .Subscribe(value =>
+ {
+ viewModel.FixedJson = value;
+ });
+
+
+ this.OnPropertyChanges(PayloadProperty)
+ .ObserveOn(SynchronizationContext.Current!)
+ .Subscribe(value =>
+ {
+ viewModel.Payload = value;
+ });
+
+
+ viewModel.PropertyChanged += (_, args) =>
+ {
+ switch (args.PropertyName)
+ {
+ case nameof(viewModel.ContentType):
+ SetValue(ContentTypeProperty, viewModel.ContentType);
+ break;
+
+ case nameof(viewModel.FixedJson):
+ SetValue(FixedJsonProperty, viewModel.FixedJson);
+ break;
+
+ case nameof(viewModel.Payload):
+ SetValue(PayloadProperty, viewModel.Payload);
+ break;
+ }
+ };
+
+ InitializeComponent();
+
+ // Setting the DataContext for the UserControl is a major PITA when binding the control's properties,
+ // so I've moved the ViewModel one level down to get the best of both worlds...
+ DataContextContainer.DataContext = viewModel;
+ }
+ }
+}
diff --git a/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.Designer.cs b/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.Designer.cs
new file mode 100644
index 0000000..aff3adc
--- /dev/null
+++ b/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.Designer.cs
@@ -0,0 +1,108 @@
+//------------------------------------------------------------------------------
+//
+// 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.UI.Tab.Publisher {
+ 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 PayloadEditorStrings {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal PayloadEditorStrings() {
+ }
+
+ ///
+ /// 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.UI.Tab.Publisher.PayloadEditorStrings", typeof(PayloadEditorStrings).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 JSON.
+ ///
+ internal static string ContentTypeJson {
+ get {
+ return ResourceManager.GetString("ContentTypeJson", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Other.
+ ///
+ internal static string ContentTypeOther {
+ get {
+ return ResourceManager.GetString("ContentTypeOther", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Plain text.
+ ///
+ internal static string ContentTypePlain {
+ get {
+ return ResourceManager.GetString("ContentTypePlain", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Invalid JSON: {0}.
+ ///
+ internal static string JsonValidationError {
+ get {
+ return ResourceManager.GetString("JsonValidationError", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Valid JSON.
+ ///
+ internal static string JsonValidationOk {
+ get {
+ return ResourceManager.GetString("JsonValidationOk", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.resx b/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.resx
new file mode 100644
index 0000000..0ae56b9
--- /dev/null
+++ b/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.resx
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+ JSON
+
+
+ Other
+
+
+ Plain text
+
+
+ Invalid JSON: {0}
+
+
+ Valid JSON
+
+
\ No newline at end of file
diff --git a/PettingZoo/UI/Tab/Publisher/PayloadEditorViewModel.cs b/PettingZoo/UI/Tab/Publisher/PayloadEditorViewModel.cs
new file mode 100644
index 0000000..1eb0149
--- /dev/null
+++ b/PettingZoo/UI/Tab/Publisher/PayloadEditorViewModel.cs
@@ -0,0 +1,154 @@
+using System;
+using System.ComponentModel;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using System.Windows;
+using Newtonsoft.Json.Linq;
+
+namespace PettingZoo.UI.Tab.Publisher
+{
+ public enum PayloadEditorContentType
+ {
+ Json,
+ Plain,
+ Other
+ };
+
+
+ public class PayloadEditorViewModel : BaseViewModel
+ {
+ private const string ContentTypeJson = "application/json";
+ private const string ContentTypePlain = "text/plain";
+
+ private string contentType = ContentTypeJson;
+ private PayloadEditorContentType contentTypeSelection = PayloadEditorContentType.Json;
+ private bool fixedJson;
+
+ private bool jsonValid = true;
+ private string jsonValidationMessage;
+
+ private string payload = "";
+
+
+ public string ContentType
+ {
+ get => ContentTypeSelection switch
+ {
+ PayloadEditorContentType.Json => ContentTypeJson,
+ PayloadEditorContentType.Plain => ContentTypePlain,
+ _ => contentType
+ };
+
+ set
+ {
+ if (!SetField(ref contentType, value))
+ return;
+
+ ContentTypeSelection = value switch
+ {
+ ContentTypeJson => PayloadEditorContentType.Json,
+ ContentTypePlain => PayloadEditorContentType.Plain,
+ _ => PayloadEditorContentType.Other
+ };
+ }
+ }
+
+
+ public PayloadEditorContentType ContentTypeSelection
+ {
+ get => contentTypeSelection;
+ set
+ {
+ if (!SetField(ref contentTypeSelection, value, otherPropertiesChanged: new [] { nameof(JsonValidationVisibility) }))
+ return;
+
+ ContentType = ContentTypeSelection switch
+ {
+ PayloadEditorContentType.Json => ContentTypeJson,
+ PayloadEditorContentType.Plain => ContentTypePlain,
+ _ => ContentType
+ };
+
+ ValidatePayload();
+ }
+ }
+
+
+ public bool FixedJson
+ {
+ get => fixedJson;
+ set => SetField(ref fixedJson, value);
+ }
+
+ public Visibility JsonValidationVisibility => ContentTypeSelection == PayloadEditorContentType.Json ? Visibility.Visible : Visibility.Collapsed;
+ public Visibility JsonValidationOk => JsonValid ? Visibility.Visible : Visibility.Collapsed;
+ public Visibility JsonValidationError => !JsonValid ? Visibility.Visible : Visibility.Collapsed;
+
+
+ public string JsonValidationMessage
+ {
+ get => jsonValidationMessage;
+ private set => SetField(ref jsonValidationMessage, value);
+ }
+
+
+ public bool JsonValid
+ {
+ get => jsonValid;
+ private set => SetField(ref jsonValid, value, otherPropertiesChanged: new[] { nameof(JsonValidationOk), nameof(JsonValidationError) });
+ }
+
+ public Visibility ContentTypeVisibility => FixedJson ? Visibility.Collapsed : Visibility.Visible;
+
+
+ public string Payload
+ {
+ get => payload;
+ set => SetField(ref payload, value);
+ }
+
+
+
+ public PayloadEditorViewModel()
+ {
+ jsonValidationMessage = PayloadEditorStrings.JsonValidationOk;
+
+ Observable.FromEventPattern(
+ h => PropertyChanged += h,
+ h => PropertyChanged -= h)
+ .Where(e => e.EventArgs.PropertyName == nameof(Payload))
+ .Throttle(TimeSpan.FromMilliseconds(500))
+ .Subscribe(_ => ValidatePayload());
+ }
+
+
+ private void ValidatePayload()
+ {
+ if (ContentTypeSelection != PayloadEditorContentType.Json)
+ {
+ JsonValid = true;
+ JsonValidationMessage = PayloadEditorStrings.JsonValidationOk;
+ return;
+ }
+
+ try
+ {
+ if (!string.IsNullOrEmpty(Payload))
+ JToken.Parse(Payload);
+
+ JsonValid = true;
+ JsonValidationMessage = PayloadEditorStrings.JsonValidationOk;
+ }
+ catch (Exception e)
+ {
+ JsonValid = false;
+ JsonValidationMessage = string.Format(PayloadEditorStrings.JsonValidationError, e.Message);
+ }
+ }
+ }
+
+
+ public class DesignTimePayloadEditorViewModel : PayloadEditorViewModel
+ {
+ }
+}
diff --git a/PettingZoo/UI/Tab/Publisher/PublisherView.xaml b/PettingZoo/UI/Tab/Publisher/PublisherView.xaml
new file mode 100644
index 0000000..7901b69
--- /dev/null
+++ b/PettingZoo/UI/Tab/Publisher/PublisherView.xaml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PettingZoo/UI/Tab/Publisher/PublisherView.xaml.cs b/PettingZoo/UI/Tab/Publisher/PublisherView.xaml.cs
new file mode 100644
index 0000000..d32221d
--- /dev/null
+++ b/PettingZoo/UI/Tab/Publisher/PublisherView.xaml.cs
@@ -0,0 +1,19 @@
+using System.Windows.Media;
+
+namespace PettingZoo.UI.Tab.Publisher
+{
+ ///
+ /// Interaction logic for PublisherView.xaml
+ ///
+ public partial class PublisherView
+ {
+ public PublisherView(PublisherViewModel viewModel)
+ {
+ DataContext = viewModel;
+ InitializeComponent();
+
+ if (!System.ComponentModel.DesignerProperties.GetIsInDesignMode(this))
+ Background = Brushes.Transparent;
+ }
+ }
+}
diff --git a/PettingZoo/UI/Tab/Publisher/PublisherViewModel.cs b/PettingZoo/UI/Tab/Publisher/PublisherViewModel.cs
new file mode 100644
index 0000000..c71365d
--- /dev/null
+++ b/PettingZoo/UI/Tab/Publisher/PublisherViewModel.cs
@@ -0,0 +1,260 @@
+using System;
+using System.Collections.Generic;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using PettingZoo.Core.Connection;
+
+namespace PettingZoo.UI.Tab.Publisher
+{
+ public enum MessageType
+ {
+ Raw,
+ Tapeti
+ }
+
+
+ public class PublisherViewModel : BaseViewModel, ITabToolbarCommands, IPublishDestination
+ {
+ private readonly IConnection connection;
+ private readonly ITabFactory tabFactory;
+ private readonly ITabHost tabHost;
+
+ private bool sendToExchange = true;
+ private string exchange = "";
+ private string routingKey = "";
+ private string queue = "";
+ private string replyTo = "";
+ private bool replyToSpecified = true;
+
+ private MessageType messageType;
+ private UserControl? messageTypeControl;
+ private ICommand? messageTypePublishCommand;
+
+ private UserControl? rawPublisherView;
+ private UserControl? tapetiPublisherView;
+
+ private readonly DelegateCommand publishCommand;
+ private readonly TabToolbarCommand[] toolbarCommands;
+
+
+ public bool SendToExchange
+ {
+ get => sendToExchange;
+ set => SetField(ref sendToExchange, value, otherPropertiesChanged: new[] { nameof(SendToQueue), nameof(ExchangeVisibility), nameof(QueueVisibility), nameof(Title) });
+ }
+
+
+ public bool SendToQueue
+ {
+ get => !SendToExchange;
+ set => SendToExchange = !value;
+ }
+
+
+ public string Exchange
+ {
+ get => exchange;
+ set => SetField(ref exchange, value);
+ }
+
+
+ public string RoutingKey
+ {
+ get => routingKey;
+ set => SetField(ref routingKey, value, otherPropertiesChanged: new[] { nameof(Title) });
+ }
+
+
+ public string Queue
+ {
+ get => queue;
+ set => SetField(ref queue, value, otherPropertiesChanged: new[] { nameof(Title) });
+ }
+
+
+ public string ReplyTo
+ {
+ get => replyTo;
+ set => SetField(ref replyTo, value);
+ }
+
+
+ public bool ReplyToSpecified
+ {
+ get => replyToSpecified;
+ set => SetField(ref replyToSpecified, value, otherPropertiesChanged: new[] { nameof(ReplyToNewSubscriber) });
+ }
+
+
+ public bool ReplyToNewSubscriber
+ {
+ get => !ReplyToSpecified;
+ set => ReplyToSpecified = !value;
+ }
+
+
+ public virtual Visibility ExchangeVisibility => SendToExchange ? Visibility.Visible : Visibility.Collapsed;
+ public virtual Visibility QueueVisibility => SendToQueue ? Visibility.Visible : Visibility.Collapsed;
+
+
+ public MessageType MessageType
+ {
+ get => messageType;
+ set
+ {
+ if (SetField(ref messageType, value,
+ otherPropertiesChanged: new[]
+ {
+ nameof(MessageTypeRaw),
+ nameof(MessageTypeTapeti)
+ }))
+ {
+ SetMessageTypeControl(value);
+ }
+ }
+ }
+
+ public bool MessageTypeRaw
+ {
+ get => MessageType == MessageType.Raw;
+ set { if (value) MessageType = MessageType.Raw; }
+ }
+
+ public bool MessageTypeTapeti
+ {
+ get => MessageType == MessageType.Tapeti;
+ set { if (value) MessageType = MessageType.Tapeti; }
+ }
+
+
+ public UserControl? MessageTypeControl
+ {
+ get => messageTypeControl;
+ set => SetField(ref messageTypeControl, value);
+ }
+
+
+ public ICommand PublishCommand => publishCommand;
+
+
+ public string Title => SendToQueue
+ ? string.IsNullOrWhiteSpace(Queue) ? PublisherViewStrings.TabTitleEmpty : string.Format(PublisherViewStrings.TabTitle, Queue)
+ : string.IsNullOrWhiteSpace(RoutingKey) ? PublisherViewStrings.TabTitleEmpty : string.Format(PublisherViewStrings.TabTitle, RoutingKey);
+
+
+ public IEnumerable ToolbarCommands => toolbarCommands;
+
+
+ string IPublishDestination.Exchange => SendToExchange ? Exchange : "";
+ string IPublishDestination.RoutingKey => SendToExchange ? RoutingKey : Queue;
+
+
+ public PublisherViewModel(ITabHost tabHost, ITabFactory tabFactory, IConnection connection, ReceivedMessageInfo? fromReceivedMessage = null)
+ {
+ this.connection = connection;
+ this.tabFactory = tabFactory;
+ this.tabHost = tabHost;
+
+ publishCommand = new DelegateCommand(PublishExecute, PublishCanExecute);
+
+ toolbarCommands = new[]
+ {
+ new TabToolbarCommand(PublishCommand, PublisherViewStrings.CommandPublish, SvgIconHelper.LoadFromResource("/Images/PublishSend.svg"))
+ };
+
+ if (fromReceivedMessage != null)
+ SetMessageTypeControl(fromReceivedMessage);
+ else
+ SetMessageTypeControl(MessageType.Raw);
+ }
+
+
+ private void PublishExecute()
+ {
+ messageTypePublishCommand?.Execute(null);
+ }
+
+
+ private bool PublishCanExecute()
+ {
+ return messageTypePublishCommand?.CanExecute(null) ?? false;
+ }
+
+
+ private void SetMessageTypeControl(MessageType value)
+ {
+ switch (value)
+ {
+ case MessageType.Raw:
+ var rawPublisherViewModel = new RawPublisherViewModel(connection, this);
+ rawPublisherView ??= new RawPublisherView(rawPublisherViewModel);
+ MessageTypeControl = rawPublisherView;
+
+ messageTypePublishCommand = rawPublisherViewModel.PublishCommand;
+ break;
+
+ case MessageType.Tapeti:
+ var tapetiPublisherViewModel = new TapetiPublisherViewModel(connection, this);
+ tapetiPublisherView ??= new TapetiPublisherView(tapetiPublisherViewModel);
+ MessageTypeControl = tapetiPublisherView;
+
+ messageTypePublishCommand = tapetiPublisherViewModel.PublishCommand;
+ break;
+
+ default:
+ throw new ArgumentException($@"Unknown message type: {value}", nameof(value));
+ }
+
+ publishCommand.RaiseCanExecuteChanged();
+ }
+
+
+ private void SetMessageTypeControl(ReceivedMessageInfo fromReceivedMessage)
+ {
+ Exchange = fromReceivedMessage.Exchange;
+ RoutingKey = fromReceivedMessage.RoutingKey;
+
+
+ if (TapetiPublisherViewModel.IsTapetiMessage(fromReceivedMessage))
+ {
+ var tapetiPublisherViewModel = new TapetiPublisherViewModel(connection, this, fromReceivedMessage);
+ tapetiPublisherView = new TapetiPublisherView(tapetiPublisherViewModel);
+
+ MessageType = MessageType.Tapeti;
+ }
+ else
+ {
+ var rawPublisherViewModel = new RawPublisherViewModel(connection, this, fromReceivedMessage);
+ rawPublisherView = new RawPublisherView(rawPublisherViewModel);
+
+ MessageType = MessageType.Raw;
+ }
+ }
+
+
+ public string? GetReplyTo()
+ {
+ if (ReplyToSpecified)
+ return string.IsNullOrEmpty(ReplyTo) ? null : ReplyTo;
+
+ var subscriber = connection.Subscribe();
+ var tab = tabFactory.CreateSubscriberTab(connection, subscriber);
+ tabHost.AddTab(tab);
+
+ subscriber.Start();
+ return subscriber.QueueName;
+ }
+ }
+
+
+ public class DesignTimePublisherViewModel : PublisherViewModel
+ {
+ public DesignTimePublisherViewModel() : base(null!, null!, null!)
+ {
+ }
+
+ public override Visibility ExchangeVisibility => Visibility.Visible;
+ public override Visibility QueueVisibility => Visibility.Visible;
+ }
+}
diff --git a/PettingZoo/UI/Tab/Publisher/PublisherViewStrings.Designer.cs b/PettingZoo/UI/Tab/Publisher/PublisherViewStrings.Designer.cs
new file mode 100644
index 0000000..e26fe49
--- /dev/null
+++ b/PettingZoo/UI/Tab/Publisher/PublisherViewStrings.Designer.cs
@@ -0,0 +1,189 @@
+//------------------------------------------------------------------------------
+//
+// 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.UI.Tab.Publisher {
+ 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()]
+ public class PublisherViewStrings {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal PublisherViewStrings() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.UI.Tab.Publisher.PublisherViewStrings", typeof(PublisherViewStrings).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)]
+ public static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Publish.
+ ///
+ public static string CommandPublish {
+ get {
+ return ResourceManager.GetString("CommandPublish", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Exchange.
+ ///
+ public static string LabelExchange {
+ get {
+ return ResourceManager.GetString("LabelExchange", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Message type: .
+ ///
+ public static string LabelMessageType {
+ get {
+ return ResourceManager.GetString("LabelMessageType", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Queue.
+ ///
+ public static string LabelQueue {
+ get {
+ return ResourceManager.GetString("LabelQueue", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Reply To.
+ ///
+ public static string LabelReplyTo {
+ get {
+ return ResourceManager.GetString("LabelReplyTo", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to New subscriber tab.
+ ///
+ public static string LabelReplyToNewSubscriber {
+ get {
+ return ResourceManager.GetString("LabelReplyToNewSubscriber", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Specified queue.
+ ///
+ public static string LabelReplyToSpecified {
+ get {
+ return ResourceManager.GetString("LabelReplyToSpecified", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Routing key.
+ ///
+ public static string LabelRoutingKey {
+ get {
+ return ResourceManager.GetString("LabelRoutingKey", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Publish to exchange (topic).
+ ///
+ public static string LabelSendToExchange {
+ get {
+ return ResourceManager.GetString("LabelSendToExchange", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Publish to queue (direct).
+ ///
+ public static string LabelSendToQueue {
+ get {
+ return ResourceManager.GetString("LabelSendToQueue", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Raw message.
+ ///
+ public static string OptionMessageTypeRaw {
+ get {
+ return ResourceManager.GetString("OptionMessageTypeRaw", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Tapeti message.
+ ///
+ public static string OptionMessageTypeTapeti {
+ get {
+ return ResourceManager.GetString("OptionMessageTypeTapeti", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Publish: {0}.
+ ///
+ public static string TabTitle {
+ get {
+ return ResourceManager.GetString("TabTitle", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Publish.
+ ///
+ public static string TabTitleEmpty {
+ get {
+ return ResourceManager.GetString("TabTitleEmpty", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/PettingZoo/UI/Tab/Publisher/PublisherViewStrings.resx b/PettingZoo/UI/Tab/Publisher/PublisherViewStrings.resx
new file mode 100644
index 0000000..d28e7b3
--- /dev/null
+++ b/PettingZoo/UI/Tab/Publisher/PublisherViewStrings.resx
@@ -0,0 +1,162 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+ Publish
+
+
+ Exchange
+
+
+ Message type:
+
+
+ Queue
+
+
+ Reply To
+
+
+ New subscriber tab
+
+
+ Specified queue
+
+
+ Routing key
+
+
+ Publish to exchange (topic)
+
+
+ Publish to queue (direct)
+
+
+ Raw message
+
+
+ Tapeti message
+
+
+ Publish: {0}
+
+
+ Publish
+
+
\ No newline at end of file
diff --git a/PettingZoo/UI/Tab/Publisher/RawPublisherView.xaml b/PettingZoo/UI/Tab/Publisher/RawPublisherView.xaml
new file mode 100644
index 0000000..567ff6a
--- /dev/null
+++ b/PettingZoo/UI/Tab/Publisher/RawPublisherView.xaml
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PettingZoo/UI/Tab/Publisher/RawPublisherView.xaml.cs b/PettingZoo/UI/Tab/Publisher/RawPublisherView.xaml.cs
new file mode 100644
index 0000000..022895e
--- /dev/null
+++ b/PettingZoo/UI/Tab/Publisher/RawPublisherView.xaml.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Threading;
+
+namespace PettingZoo.UI.Tab.Publisher
+{
+ ///
+ /// Interaction logic for RawPublisherView.xaml
+ ///
+ public partial class RawPublisherView
+ {
+ private readonly RawPublisherViewModel viewModel;
+ private readonly DispatcherTimer checkEmptyHeaderTimer;
+
+
+ public RawPublisherView(RawPublisherViewModel viewModel)
+ {
+ this.viewModel = viewModel;
+
+ DataContext = viewModel;
+ InitializeComponent();
+
+ checkEmptyHeaderTimer = new DispatcherTimer();
+ checkEmptyHeaderTimer.Tick += CheckEmptyHeaderTimerOnTick;
+ checkEmptyHeaderTimer.Interval = TimeSpan.FromMilliseconds(50);
+ }
+
+ private void Header_OnLostFocus(object sender, RoutedEventArgs e)
+ {
+ var dataContext = (sender as FrameworkElement)?.DataContext;
+ if (dataContext is not RawPublisherViewModel.Header header)
+ return;
+
+ if (!header.IsEmpty())
+ return;
+
+ // At this point the focused element is null, so we need to check again in a bit. This will prevent
+ // the header line from being removed when jumping between empty key and value textboxes
+ checkEmptyHeaderTimer.Stop();
+ checkEmptyHeaderTimer.Start();
+ }
+
+
+ private void CheckEmptyHeaderTimerOnTick(object? sender, EventArgs e)
+ {
+ checkEmptyHeaderTimer.Stop();
+
+ RawPublisherViewModel.Header? focusedHeader = null;
+
+ var focusedControl = Keyboard.FocusedElement;
+ if (focusedControl is FrameworkElement { DataContext: RawPublisherViewModel.Header header })
+ focusedHeader = header;
+
+ var emptyheaders = viewModel.Headers
+ .Take(viewModel.Headers.Count - 1)
+ .Where(h => h != focusedHeader && h.IsEmpty())
+ .ToArray();
+
+ foreach (var emptyHeader in emptyheaders)
+ viewModel.Headers.Remove(emptyHeader);
+ }
+ }
+}
diff --git a/PettingZoo/UI/Tab/Publisher/RawPublisherViewModel.cs b/PettingZoo/UI/Tab/Publisher/RawPublisherViewModel.cs
new file mode 100644
index 0000000..e163462
--- /dev/null
+++ b/PettingZoo/UI/Tab/Publisher/RawPublisherViewModel.cs
@@ -0,0 +1,299 @@
+using System;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Text;
+using System.Windows;
+using System.Windows.Input;
+using PettingZoo.Core.Connection;
+
+namespace PettingZoo.UI.Tab.Publisher
+{
+ public class RawPublisherViewModel : BaseViewModel
+ {
+ private readonly IConnection connection;
+ private readonly IPublishDestination publishDestination;
+ private readonly DelegateCommand publishCommand;
+ private readonly DelegateCommand propertiesExpandCollapseCommand;
+ private bool propertiesExpanded;
+
+ private MessageDeliveryMode deliveryMode;
+
+ private string contentType = "application/json";
+ private string correlationId = "";
+ private string appId = "";
+ private string contentEncoding = "";
+ private string expiration = "";
+ private string messageId = "";
+ private string priority = "";
+ private string timestamp = "";
+ private string typeProperty = "";
+ private string userId = "";
+ private string payload = "";
+
+
+
+ public int DeliveryModeIndex
+ {
+ get => deliveryMode == MessageDeliveryMode.Persistent ? 1 : 0;
+ set => SetField(ref deliveryMode, value == 1 ? MessageDeliveryMode.Persistent : MessageDeliveryMode.NonPersistent);
+ }
+
+
+ public string ContentType
+ {
+ get => contentType;
+ set => SetField(ref contentType, value);
+ }
+
+
+ public string CorrelationId
+ {
+ get => correlationId;
+ set => SetField(ref correlationId, value);
+ }
+
+
+ public string AppId
+ {
+ get => appId;
+ set => SetField(ref appId, value);
+ }
+
+
+ public string ContentEncoding
+ {
+ get => contentEncoding;
+ set => SetField(ref contentEncoding, value);
+ }
+
+
+ public string Expiration
+ {
+ get => expiration;
+ set => SetField(ref expiration, value);
+ }
+
+
+ public string MessageId
+ {
+ get => messageId;
+ set => SetField(ref messageId, value);
+ }
+
+
+ public string Priority
+ {
+ get => priority;
+ set => SetField(ref priority, value);
+ }
+
+
+ public string Timestamp
+ {
+ get => timestamp;
+ set => SetField(ref timestamp, value);
+ }
+
+
+ public string TypeProperty
+ {
+ get => typeProperty;
+ set => SetField(ref typeProperty, value);
+ }
+
+
+ public string UserId
+ {
+ get => userId;
+ set => SetField(ref userId, value);
+ }
+
+
+ public string Payload
+ {
+ get => payload;
+ set => SetField(ref payload, value);
+ }
+
+
+ public ObservableCollection Headers { get; } = new();
+
+
+ public ICommand PublishCommand => publishCommand;
+ public ICommand PropertiesExpandCollapseCommand => propertiesExpandCollapseCommand;
+
+
+ public bool PropertiesExpanded
+ {
+ get => propertiesExpanded;
+ set => SetField(ref propertiesExpanded, value, otherPropertiesChanged: new[]
+ {
+ nameof(PropertiesExpandedVisibility),
+ nameof(PropertiesExpandedCollapsedText)
+ });
+ }
+
+ public Visibility PropertiesExpandedVisibility => propertiesExpanded ? Visibility.Visible : Visibility.Collapsed;
+ public string PropertiesExpandedCollapsedText => propertiesExpanded
+ ? RawPublisherViewStrings.PropertiesCollapse
+ : RawPublisherViewStrings.PropertiesExpand;
+
+
+ protected Header LastHeader;
+
+
+ public RawPublisherViewModel(IConnection connection, IPublishDestination publishDestination, ReceivedMessageInfo? receivedMessage = null)
+ {
+ this.connection = connection;
+ this.publishDestination = publishDestination;
+
+ publishCommand = new DelegateCommand(PublishExecute, PublishCanExecute);
+ propertiesExpandCollapseCommand = new DelegateCommand(PropertiesExpandCollapseExecute);
+
+ if (receivedMessage != null)
+ {
+ CorrelationId = receivedMessage.Properties.CorrelationId ?? "";
+ Priority = receivedMessage.Properties.Priority?.ToString() ?? "";
+ AppId = receivedMessage.Properties.AppId ?? "";
+ ContentEncoding = receivedMessage.Properties.ContentEncoding ?? "";
+ ContentType = receivedMessage.Properties.ContentType ?? "";
+ Expiration = receivedMessage.Properties.Expiration ?? "";
+ MessageId = receivedMessage.Properties.MessageId ?? "";
+ Timestamp = receivedMessage.Properties.Timestamp?.ToString() ?? "";
+ TypeProperty = receivedMessage.Properties.Type ?? "";
+ UserId = receivedMessage.Properties.UserId ?? "";
+
+ Payload = Encoding.UTF8.GetString(receivedMessage.Body);
+
+ foreach (var header in receivedMessage.Properties.Headers)
+ Headers.Add(new Header
+ {
+ Key = header.Key,
+ Value = header.Value
+ });
+
+ PropertiesExpanded = AnyNotEmpty(AppId, ContentEncoding, Expiration, MessageId, Priority, Timestamp, TypeProperty, UserId);
+ }
+
+ AddHeader();
+ }
+
+
+ private static bool AnyNotEmpty(params string?[] values)
+ {
+ return values.Any(s => !string.IsNullOrEmpty(s));
+ }
+
+
+ private void LastHeaderChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ LastHeader.PropertyChanged -= LastHeaderChanged;
+ AddHeader();
+ }
+
+
+ [MemberNotNull(nameof(LastHeader))]
+ private void AddHeader()
+ {
+ LastHeader = new Header();
+ LastHeader.PropertyChanged += LastHeaderChanged;
+ Headers.Add(LastHeader);
+ }
+
+
+ private void PropertiesExpandCollapseExecute()
+ {
+ PropertiesExpanded = !PropertiesExpanded;
+ }
+
+
+ private void PublishExecute()
+ {
+ static string? NullIfEmpty(string? value)
+ {
+ return string.IsNullOrEmpty(value) ? null : value;
+ }
+
+ // TODO check parsing of priority and timestamp
+
+ var headers = Headers.Where(h => h.IsValid()).ToDictionary(h => h.Key, h => h.Value);
+
+ // TODO background worker / async
+
+ connection.Publish(new PublishMessageInfo(
+ publishDestination.Exchange,
+ publishDestination.RoutingKey,
+ Encoding.UTF8.GetBytes(Payload),
+ new MessageProperties(headers)
+ {
+ AppId = NullIfEmpty(AppId),
+ ContentEncoding = NullIfEmpty(ContentEncoding),
+ ContentType = NullIfEmpty(ContentType),
+ CorrelationId = NullIfEmpty(CorrelationId),
+ DeliveryMode = deliveryMode,
+ Expiration = NullIfEmpty(Expiration),
+ MessageId = NullIfEmpty(MessageId),
+ Priority = !string.IsNullOrEmpty(Priority) && byte.TryParse(Priority, out var priorityValue) ? priorityValue : null,
+ ReplyTo = publishDestination.GetReplyTo(),
+ Timestamp = !string.IsNullOrEmpty(Timestamp) && DateTime.TryParse(Timestamp, out var timestampValue) ? timestampValue : null,
+ Type = NullIfEmpty(TypeProperty),
+ UserId = NullIfEmpty(UserId)
+ }));
+ }
+
+
+ private bool PublishCanExecute()
+ {
+ // TODO validate input
+ return true;
+ }
+
+
+ public class Header : BaseViewModel
+ {
+ private string key = "";
+ private string value = "";
+
+
+ public string Key
+ {
+ get => key;
+ set => SetField(ref key, value);
+ }
+
+
+ public string Value
+ {
+ get => value;
+ set => SetField(ref this.value, value);
+ }
+
+
+ public bool IsEmpty()
+ {
+ return string.IsNullOrEmpty(Key) && string.IsNullOrEmpty(Value);
+ }
+
+
+ public bool IsValid()
+ {
+ return !string.IsNullOrEmpty(Key) && !string.IsNullOrEmpty(Value);
+ }
+ }
+ }
+
+
+ public class DesignTimeRawPublisherViewModel : RawPublisherViewModel
+ {
+ public DesignTimeRawPublisherViewModel() : base(null!, null!)
+ {
+ PropertiesExpanded = true;
+
+ var capturedLastHeader = LastHeader;
+ capturedLastHeader.Key = "Example";
+ capturedLastHeader.Value = "header";
+ }
+ }
+}
diff --git a/PettingZoo/UI/Tab/Publisher/RawPublisherViewStrings.Designer.cs b/PettingZoo/UI/Tab/Publisher/RawPublisherViewStrings.Designer.cs
new file mode 100644
index 0000000..e771a60
--- /dev/null
+++ b/PettingZoo/UI/Tab/Publisher/RawPublisherViewStrings.Designer.cs
@@ -0,0 +1,243 @@
+//------------------------------------------------------------------------------
+//
+// 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.UI.Tab.Publisher {
+ 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()]
+ public class RawPublisherViewStrings {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal RawPublisherViewStrings() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.UI.Tab.Publisher.RawPublisherViewStrings", typeof(RawPublisherViewStrings).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)]
+ public static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Transient (non-persistent).
+ ///
+ public static string DeliveryModeNonPersistent {
+ get {
+ return ResourceManager.GetString("DeliveryModeNonPersistent", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Persistent.
+ ///
+ public static string DeliveryModePersistent {
+ get {
+ return ResourceManager.GetString("DeliveryModePersistent", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Name.
+ ///
+ public static string HeaderName {
+ get {
+ return ResourceManager.GetString("HeaderName", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Value.
+ ///
+ public static string HeaderValue {
+ get {
+ return ResourceManager.GetString("HeaderValue", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to App ID.
+ ///
+ public static string LabelAppId {
+ get {
+ return ResourceManager.GetString("LabelAppId", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Content encoding.
+ ///
+ public static string LabelContentEncoding {
+ get {
+ return ResourceManager.GetString("LabelContentEncoding", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Content type.
+ ///
+ public static string LabelContentType {
+ get {
+ return ResourceManager.GetString("LabelContentType", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Correlation ID.
+ ///
+ public static string LabelCorrelationId {
+ get {
+ return ResourceManager.GetString("LabelCorrelationId", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Delivery mode.
+ ///
+ public static string LabelDeliveryMode {
+ get {
+ return ResourceManager.GetString("LabelDeliveryMode", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Expiration.
+ ///
+ public static string LabelExpiration {
+ get {
+ return ResourceManager.GetString("LabelExpiration", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Headers.
+ ///
+ public static string LabelHeaders {
+ get {
+ return ResourceManager.GetString("LabelHeaders", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Message ID.
+ ///
+ public static string LabelMessageId {
+ get {
+ return ResourceManager.GetString("LabelMessageId", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Payload.
+ ///
+ public static string LabelPayload {
+ get {
+ return ResourceManager.GetString("LabelPayload", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Priority.
+ ///
+ public static string LabelPriority {
+ get {
+ return ResourceManager.GetString("LabelPriority", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Properties.
+ ///
+ public static string LabelProperties {
+ get {
+ return ResourceManager.GetString("LabelProperties", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Timestamp.
+ ///
+ public static string LabelTimestamp {
+ get {
+ return ResourceManager.GetString("LabelTimestamp", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Type.
+ ///
+ public static string LabelType {
+ get {
+ return ResourceManager.GetString("LabelType", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to User ID.
+ ///
+ public static string LabelUserId {
+ get {
+ return ResourceManager.GetString("LabelUserId", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to ⏶ Collapse.
+ ///
+ public static string PropertiesCollapse {
+ get {
+ return ResourceManager.GetString("PropertiesCollapse", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to ⏷ Expand.
+ ///
+ public static string PropertiesExpand {
+ get {
+ return ResourceManager.GetString("PropertiesExpand", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/PettingZoo/UI/Tab/Publisher/RawPublisherViewStrings.resx b/PettingZoo/UI/Tab/Publisher/RawPublisherViewStrings.resx
new file mode 100644
index 0000000..628f2d6
--- /dev/null
+++ b/PettingZoo/UI/Tab/Publisher/RawPublisherViewStrings.resx
@@ -0,0 +1,180 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+ Transient (non-persistent)
+
+
+ Persistent
+
+
+ Name
+
+
+ Value
+
+
+ App ID
+
+
+ Content encoding
+
+
+ Content type
+
+
+ Correlation ID
+
+
+ Delivery mode
+
+
+ Expiration
+
+
+ Headers
+
+
+ Message ID
+
+
+ Payload
+
+
+ Priority
+
+
+ Properties
+
+
+ Timestamp
+
+
+ Type
+
+
+ User ID
+
+
+ ⏶ Collapse
+
+
+ ⏷ Expand
+
+
\ No newline at end of file
diff --git a/PettingZoo/UI/Tab/Publisher/TapetiPublisherView.xaml b/PettingZoo/UI/Tab/Publisher/TapetiPublisherView.xaml
new file mode 100644
index 0000000..836dbee
--- /dev/null
+++ b/PettingZoo/UI/Tab/Publisher/TapetiPublisherView.xaml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PettingZoo/UI/Tab/Publisher/TapetiPublisherView.xaml.cs b/PettingZoo/UI/Tab/Publisher/TapetiPublisherView.xaml.cs
new file mode 100644
index 0000000..bde91cb
--- /dev/null
+++ b/PettingZoo/UI/Tab/Publisher/TapetiPublisherView.xaml.cs
@@ -0,0 +1,26 @@
+using System.Windows;
+using System.Windows.Controls;
+
+namespace PettingZoo.UI.Tab.Publisher
+{
+ ///
+ /// Interaction logic for TapetiPublisherView.xaml
+ ///
+ public partial class TapetiPublisherView
+ {
+ public TapetiPublisherView(TapetiPublisherViewModel viewModel)
+ {
+ DataContext = viewModel;
+ InitializeComponent();
+ }
+
+
+ private void CaretToEnd(object sender, RoutedEventArgs e)
+ {
+ if (sender is not TextBox textBox)
+ return;
+
+ textBox.CaretIndex = textBox.Text.Length;
+ }
+ }
+}
diff --git a/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewModel.cs b/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewModel.cs
new file mode 100644
index 0000000..6c04620
--- /dev/null
+++ b/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewModel.cs
@@ -0,0 +1,146 @@
+using System.Collections.Generic;
+using System.Text;
+using System.Windows.Input;
+using PettingZoo.Core.Connection;
+using IConnection = PettingZoo.Core.Connection.IConnection;
+
+namespace PettingZoo.UI.Tab.Publisher
+{
+ public class TapetiPublisherViewModel : BaseViewModel
+ {
+ private readonly IConnection connection;
+ private readonly IPublishDestination publishDestination;
+ private readonly DelegateCommand publishCommand;
+
+ private string correlationId = "";
+ private string payload = "";
+ private string className = "";
+ private string assemblyName = "";
+
+
+ public string CorrelationId
+ {
+ get => correlationId;
+ set => SetField(ref correlationId, value);
+ }
+
+
+ public string ClassName
+ {
+ get => string.IsNullOrEmpty(className) ? AssemblyName + "." : className;
+ set => SetField(ref className, value);
+ }
+
+
+ public string AssemblyName
+ {
+ get => assemblyName;
+ set => SetField(ref assemblyName, value, otherPropertiesChanged:
+ string.IsNullOrEmpty(value) || string.IsNullOrEmpty(className)
+ ? new [] { nameof(ClassName) }
+ : null
+ );
+ }
+
+
+ public string Payload
+ {
+ get => payload;
+ set => SetField(ref payload, value);
+ }
+
+
+ public ICommand PublishCommand => publishCommand;
+
+
+
+ public static bool IsTapetiMessage(ReceivedMessageInfo receivedMessage)
+ {
+ return IsTapetiMessage(receivedMessage, out _, out _);
+ }
+
+
+ public static bool IsTapetiMessage(ReceivedMessageInfo receivedMessage, out string assemblyName, out string className)
+ {
+ assemblyName = "";
+ className = "";
+
+ if (receivedMessage.Properties.ContentType != @"application/json")
+ return false;
+
+ if (!receivedMessage.Properties.Headers.TryGetValue(@"classType", out var classType))
+ return false;
+
+ var parts = classType.Split(':');
+ if (parts.Length != 2)
+ return false;
+
+ className = parts[0];
+ assemblyName = parts[1];
+ return true;
+ }
+
+
+ public TapetiPublisherViewModel(IConnection connection, IPublishDestination publishDestination, ReceivedMessageInfo? receivedMessage = null)
+ {
+ this.connection = connection;
+ this.publishDestination = publishDestination;
+
+ publishCommand = new DelegateCommand(PublishExecute, PublishCanExecute);
+
+
+ if (receivedMessage == null || !IsTapetiMessage(receivedMessage, out var receivedAssemblyName, out var receivedClassName))
+ return;
+
+ AssemblyName = receivedAssemblyName;
+ ClassName = receivedClassName;
+ CorrelationId = receivedMessage.Properties.CorrelationId ?? "";
+ Payload = Encoding.UTF8.GetString(receivedMessage.Body);
+ }
+
+
+ private void PublishExecute()
+ {
+ static string? NullIfEmpty(string? value)
+ {
+ return string.IsNullOrEmpty(value) ? null : value;
+ }
+
+ // TODO background worker / async
+
+ connection.Publish(new PublishMessageInfo(
+ publishDestination.Exchange,
+ publishDestination.RoutingKey,
+ Encoding.UTF8.GetBytes(Payload),
+ new MessageProperties(new Dictionary
+ {
+ { @"classType", $"{ClassName}:{AssemblyName}" }
+ })
+ {
+ ContentType = @"application/json",
+ CorrelationId = NullIfEmpty(CorrelationId),
+ DeliveryMode = MessageDeliveryMode.Persistent,
+ ReplyTo = publishDestination.GetReplyTo()
+ }));
+ }
+
+
+ private bool PublishCanExecute()
+ {
+ // TODO validate input
+ return true;
+ }
+ }
+
+
+ public class DesignTimeTapetiPublisherViewModel : TapetiPublisherViewModel
+ {
+ public DesignTimeTapetiPublisherViewModel() : base(null!, null!)
+ {
+ AssemblyName = "Messaging.Example";
+ ClassName = "Messaging.Example.ExampleMessage";
+ CorrelationId = "2c702859-bbbc-454e-87e2-4220c8c595d7";
+ Payload = "{\r\n \"Hello\": \"world!\"\r\n}";
+ }
+ }
+}
diff --git a/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewStrings.Designer.cs b/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewStrings.Designer.cs
new file mode 100644
index 0000000..4aa5b28
--- /dev/null
+++ b/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewStrings.Designer.cs
@@ -0,0 +1,144 @@
+//------------------------------------------------------------------------------
+//
+// 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.UI.Tab.Publisher {
+ 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()]
+ public class TapetiPublisherViewStrings {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal TapetiPublisherViewStrings() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.UI.Tab.Publisher.TapetiPublisherViewStrings", typeof(TapetiPublisherViewStrings).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)]
+ public static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to ....
+ ///
+ public static string ButtonBrowseClass {
+ get {
+ return ResourceManager.GetString("ButtonBrowseClass", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Transient (non-persistent).
+ ///
+ public static string DeliveryModeNonPersistent {
+ get {
+ return ResourceManager.GetString("DeliveryModeNonPersistent", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Persistent.
+ ///
+ public static string DeliveryModePersistent {
+ get {
+ return ResourceManager.GetString("DeliveryModePersistent", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Assembly name.
+ ///
+ public static string LabelAssemblyName {
+ get {
+ return ResourceManager.GetString("LabelAssemblyName", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Class name (full).
+ ///
+ public static string LabelClassName {
+ get {
+ return ResourceManager.GetString("LabelClassName", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Correlation ID.
+ ///
+ public static string LabelCorrelationId {
+ get {
+ return ResourceManager.GetString("LabelCorrelationId", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Delivery mode.
+ ///
+ public static string LabelDeliveryMode {
+ get {
+ return ResourceManager.GetString("LabelDeliveryMode", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Payload.
+ ///
+ public static string LabelPayload {
+ get {
+ return ResourceManager.GetString("LabelPayload", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Properties.
+ ///
+ public static string LabelProperties {
+ get {
+ return ResourceManager.GetString("LabelProperties", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewStrings.resx b/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewStrings.resx
new file mode 100644
index 0000000..7cf5efb
--- /dev/null
+++ b/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewStrings.resx
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+ ...
+
+
+ Transient (non-persistent)
+
+
+ Persistent
+
+
+ Assembly name
+
+
+ Class name (full)
+
+
+ Correlation ID
+
+
+ Delivery mode
+
+
+ Payload
+
+
+ Properties
+
+
\ No newline at end of file
diff --git a/PettingZoo/UI/Tab/Subscriber/SubscriberView.xaml b/PettingZoo/UI/Tab/Subscriber/SubscriberView.xaml
new file mode 100644
index 0000000..95a1f1b
--- /dev/null
+++ b/PettingZoo/UI/Tab/Subscriber/SubscriberView.xaml
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PettingZoo/UI/Tab/Subscriber/SubscriberView.xaml.cs b/PettingZoo/UI/Tab/Subscriber/SubscriberView.xaml.cs
new file mode 100644
index 0000000..e848167
--- /dev/null
+++ b/PettingZoo/UI/Tab/Subscriber/SubscriberView.xaml.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+
+namespace PettingZoo.UI.Tab.Subscriber
+{
+ ///
+ /// Interaction logic for SubscriberView.xaml
+ ///
+ public partial class SubscriberView
+ {
+ public SubscriberView(SubscriberViewModel viewModel)
+ {
+ DataContext = viewModel;
+ InitializeComponent();
+
+
+ if (!System.ComponentModel.DesignerProperties.GetIsInDesignMode(this))
+ Background = Brushes.Transparent;
+ }
+
+ 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);
+ }
+ }
+}
diff --git a/PettingZoo/UI/Tab/Subscriber/SubscriberViewModel.cs b/PettingZoo/UI/Tab/Subscriber/SubscriberViewModel.cs
new file mode 100644
index 0000000..b4768e6
--- /dev/null
+++ b/PettingZoo/UI/Tab/Subscriber/SubscriberViewModel.cs
@@ -0,0 +1,191 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Input;
+using PettingZoo.Core.Connection;
+using PettingZoo.Core.Rendering;
+
+// TODO visual hint of where the last read message was when activating the tab again
+
+namespace PettingZoo.UI.Tab.Subscriber
+{
+ public class SubscriberViewModel : BaseViewModel, ITabToolbarCommands, ITabActivate
+ {
+ private readonly ITabHost tabHost;
+ private readonly ITabFactory tabFactory;
+ private readonly IConnection connection;
+ private readonly ISubscriber subscriber;
+ private readonly TaskScheduler uiScheduler;
+ private ReceivedMessageInfo? selectedMessage;
+ private readonly DelegateCommand clearCommand;
+ private readonly TabToolbarCommand[] toolbarCommands;
+ private IDictionary? selectedMessageProperties;
+
+ private readonly DelegateCommand createPublisherCommand;
+
+ private bool tabActive;
+ private int unreadCount;
+
+
+ public ICommand ClearCommand => clearCommand;
+
+ // ReSharper disable once UnusedMember.Global - it is, but via a proxy
+ public ICommand CreatePublisherCommand => createPublisherCommand;
+
+ public ObservableCollection Messages { get; }
+
+ public ReceivedMessageInfo? SelectedMessage
+ {
+ get => selectedMessage;
+ set
+ {
+ if (SetField(ref selectedMessage, value, otherPropertiesChanged: new[] { nameof(SelectedMessageBody) }))
+ UpdateSelectedMessageProperties();
+ }
+ }
+
+ public string SelectedMessageBody =>
+ SelectedMessage != null
+ ? MessageBodyRenderer.Render(SelectedMessage.Body, SelectedMessage.Properties.ContentType)
+ : "";
+
+ public IDictionary? SelectedMessageProperties
+ {
+ get => selectedMessageProperties;
+ set => SetField(ref selectedMessageProperties, value);
+ }
+
+ public string Title =>
+ (subscriber.Exchange != null ? $"{subscriber.Exchange} - {subscriber.RoutingKey}" : $"{subscriber.QueueName}") +
+ (tabActive || unreadCount == 0 ? "" : $" ({unreadCount})");
+ public IEnumerable ToolbarCommands => toolbarCommands;
+
+
+ public SubscriberViewModel(ITabHost tabHost, ITabFactory tabFactory, IConnection connection, ISubscriber subscriber)
+ {
+ this.tabHost = tabHost;
+ this.tabFactory = tabFactory;
+ this.connection = connection;
+ this.subscriber = subscriber;
+
+ uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
+
+ Messages = new ObservableCollection();
+ clearCommand = new DelegateCommand(ClearExecute, ClearCanExecute);
+
+ toolbarCommands = new[]
+ {
+ new TabToolbarCommand(ClearCommand, SubscriberViewStrings.CommandClear, SvgIconHelper.LoadFromResource("/Images/Clear.svg"))
+ };
+
+ createPublisherCommand = new DelegateCommand(CreatePublisherExecute, CreatePublisherCanExecute);
+
+ subscriber.MessageReceived += SubscriberMessageReceived;
+ subscriber.Start();
+ }
+
+
+ private void ClearExecute()
+ {
+ Messages.Clear();
+ clearCommand.RaiseCanExecuteChanged();
+ }
+
+
+ private bool ClearCanExecute()
+ {
+ return Messages.Count > 0;
+ }
+
+
+ private void CreatePublisherExecute()
+ {
+ var publisherTab = tabFactory.CreatePublisherTab(connection, SelectedMessage);
+ tabHost.AddTab(publisherTab);
+ }
+
+
+ private bool CreatePublisherCanExecute()
+ {
+ return SelectedMessage != null;
+ }
+
+
+ private void SubscriberMessageReceived(object? sender, MessageReceivedEventArgs args)
+ {
+ RunFromUiScheduler(() =>
+ {
+ if (!tabActive)
+ {
+ unreadCount++;
+ RaisePropertyChanged(nameof(Title));
+ }
+
+ Messages.Add(args.MessageInfo);
+ clearCommand.RaiseCanExecuteChanged();
+ });
+ }
+
+
+ private void UpdateSelectedMessageProperties()
+ {
+ createPublisherCommand.RaiseCanExecuteChanged();
+
+ SelectedMessageProperties = SelectedMessage != null
+ ? MessagePropertiesRenderer.Render(SelectedMessage.Properties)
+ : null;
+ }
+
+
+ private void RunFromUiScheduler(Action action)
+ {
+ _ = Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.None, uiScheduler);
+ }
+
+
+ public void Activate()
+ {
+ tabActive = true;
+ unreadCount = 0;
+
+ RaisePropertyChanged(nameof(Title));
+ }
+
+ public void Deactivate()
+ {
+ tabActive = false;
+ }
+ }
+
+
+ public class DesignTimeSubscriberViewModel : SubscriberViewModel
+ {
+ public DesignTimeSubscriberViewModel() : base(null!, null!, null!, new DesignTimeSubscriber())
+ {
+ }
+
+
+ private class DesignTimeSubscriber : ISubscriber
+ {
+ public ValueTask DisposeAsync()
+ {
+ return default;
+ }
+
+
+ public string QueueName => "dummy";
+ public string Exchange => "dummy";
+ public string RoutingKey => "dummy";
+
+ #pragma warning disable CS0067
+ public event EventHandler? MessageReceived;
+ #pragma warning restore CS0067
+
+ public void Start()
+ {
+ }
+ }
+ }
+}
diff --git a/PettingZoo/UI/Tab/Subscriber/SubscriberViewStrings.Designer.cs b/PettingZoo/UI/Tab/Subscriber/SubscriberViewStrings.Designer.cs
new file mode 100644
index 0000000..11f6be5
--- /dev/null
+++ b/PettingZoo/UI/Tab/Subscriber/SubscriberViewStrings.Designer.cs
@@ -0,0 +1,135 @@
+//------------------------------------------------------------------------------
+//
+// 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.UI.Tab.Subscriber {
+ 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()]
+ public class SubscriberViewStrings {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal SubscriberViewStrings() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.UI.Tab.Subscriber.SubscriberViewStrings", typeof(SubscriberViewStrings).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)]
+ public static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Clear.
+ ///
+ public static string CommandClear {
+ get {
+ return ResourceManager.GetString("CommandClear", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Open in new Publisher tab.
+ ///
+ public static string ContextPublish {
+ get {
+ return ResourceManager.GetString("ContextPublish", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Non-persistent.
+ ///
+ public static string DeliveryModeNonPersistent {
+ get {
+ return ResourceManager.GetString("DeliveryModeNonPersistent", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Persistent.
+ ///
+ public static string DeliveryModePersistent {
+ get {
+ return ResourceManager.GetString("DeliveryModePersistent", 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);
+ }
+ }
+ }
+}
diff --git a/PettingZoo/UI/Tab/Subscriber/SubscriberViewStrings.resx b/PettingZoo/UI/Tab/Subscriber/SubscriberViewStrings.resx
new file mode 100644
index 0000000..459982b
--- /dev/null
+++ b/PettingZoo/UI/Tab/Subscriber/SubscriberViewStrings.resx
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+ Clear
+
+
+ Open in new Publisher tab
+
+
+ Non-persistent
+
+
+ Persistent
+
+
+ Body
+
+
+ Properties
+
+
+ Name
+
+
+ Value
+
+
\ No newline at end of file
diff --git a/PettingZoo/UI/Tab/Undocked/UndockedTabHostStrings.Designer.cs b/PettingZoo/UI/Tab/Undocked/UndockedTabHostStrings.Designer.cs
new file mode 100644
index 0000000..d7cd5ee
--- /dev/null
+++ b/PettingZoo/UI/Tab/Undocked/UndockedTabHostStrings.Designer.cs
@@ -0,0 +1,72 @@
+//------------------------------------------------------------------------------
+//
+// 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.UI.Tab.Undocked {
+ 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()]
+ public class UndockedTabHostStrings {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal UndockedTabHostStrings() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.UI.Tab.Undocked.UndockedTabHostStrings", typeof(UndockedTabHostStrings).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)]
+ public static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Dock tab.
+ ///
+ public static string CommandDock {
+ get {
+ return ResourceManager.GetString("CommandDock", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/PettingZoo/UI/Tab/Undocked/UndockedTabHostStrings.resx b/PettingZoo/UI/Tab/Undocked/UndockedTabHostStrings.resx
new file mode 100644
index 0000000..d987b57
--- /dev/null
+++ b/PettingZoo/UI/Tab/Undocked/UndockedTabHostStrings.resx
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+ Dock tab
+
+
\ No newline at end of file
diff --git a/PettingZoo/UI/Tab/Undocked/UndockedTabHostViewModel.cs b/PettingZoo/UI/Tab/Undocked/UndockedTabHostViewModel.cs
new file mode 100644
index 0000000..ca07e96
--- /dev/null
+++ b/PettingZoo/UI/Tab/Undocked/UndockedTabHostViewModel.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace PettingZoo.UI.Tab.Undocked
+{
+ public class UndockedTabHostViewModel : BaseViewModel
+ {
+ private readonly ITabHost tabHost;
+ private readonly ITab tab;
+ private readonly DelegateCommand dockCommand;
+
+
+ public string Title => tab.Title;
+ public ContentControl Content => tab.Content;
+ public IEnumerable ToolbarCommands => tab.ToolbarCommands;
+
+ public Visibility ToolbarCommandsSeparatorVisibility =>
+ ToolbarCommands.Any() ? Visibility.Visible : Visibility.Collapsed;
+
+ public ICommand DockCommand => dockCommand;
+
+
+ public UndockedTabHostViewModel(ITabHost tabHost, ITab tab)
+ {
+ this.tabHost = tabHost;
+ this.tab = tab;
+
+ tab.PropertyChanged += (_, args) =>
+ {
+ RaisePropertyChanged(args.PropertyName);
+ if (args.PropertyName == nameof(ToolbarCommands))
+ RaisePropertyChanged(nameof(ToolbarCommandsSeparatorVisibility));
+ };
+
+ dockCommand = new DelegateCommand(DockCommandExecute);
+ }
+
+
+ private void DockCommandExecute()
+ {
+ tabHost.DockTab(tab);
+ }
+
+
+ public void WindowClosed()
+ {
+ tabHost.UndockedTabClosed(tab);
+ }
+ }
+
+
+ public class DesignTimeUndockedTabHostViewModel : UndockedTabHostViewModel
+ {
+ public DesignTimeUndockedTabHostViewModel() : base(null!, new DesignTimeTab())
+ {
+ }
+
+
+ private class DesignTimeTab : ITab
+ {
+ public event PropertyChangedEventHandler? PropertyChanged;
+ public IEnumerable ToolbarCommands { get; } = Array.Empty();
+
+ public string Title => "Design-time tab title";
+ public ContentControl Content => null!;
+
+
+ public void Activate()
+ {
+ // Just to prevent the "PropertyChanged is never used" message
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(null));
+ }
+
+
+ public void Deactivate()
+ {
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/PettingZoo/UI/Tab/Undocked/UndockedTabHostWindow.xaml b/PettingZoo/UI/Tab/Undocked/UndockedTabHostWindow.xaml
new file mode 100644
index 0000000..2b9b461
--- /dev/null
+++ b/PettingZoo/UI/Tab/Undocked/UndockedTabHostWindow.xaml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PettingZoo/UI/Tab/Undocked/UndockedTabHostWindow.xaml.cs b/PettingZoo/UI/Tab/Undocked/UndockedTabHostWindow.xaml.cs
new file mode 100644
index 0000000..21ac421
--- /dev/null
+++ b/PettingZoo/UI/Tab/Undocked/UndockedTabHostWindow.xaml.cs
@@ -0,0 +1,47 @@
+using System.Windows;
+using System.Windows.Controls;
+
+namespace PettingZoo.UI.Tab.Undocked
+{
+ ///
+ /// Interaction logic for UndockedTabHostWindow.xaml
+ ///
+ public partial class UndockedTabHostWindow
+ {
+ public static UndockedTabHostWindow Create(ITabHost tabHost, ITab tab, double width, double height)
+ {
+ var viewModel = new UndockedTabHostViewModel(tabHost, tab);
+ var window = new UndockedTabHostWindow(viewModel)
+ {
+ Width = width,
+ Height = height
+ };
+
+ return window;
+ }
+
+
+ public UndockedTabHostWindow(UndockedTabHostViewModel viewModel)
+ {
+ DataContext = viewModel;
+ InitializeComponent();
+
+ Closed += (_, _) =>
+ {
+ viewModel.WindowClosed();
+ };
+ }
+
+ 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);
+ }
+ }
+}
diff --git a/PettingZoo/UI/Tab/ViewTab.cs b/PettingZoo/UI/Tab/ViewTab.cs
new file mode 100644
index 0000000..22950dc
--- /dev/null
+++ b/PettingZoo/UI/Tab/ViewTab.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace PettingZoo.UI.Tab
+{
+ public class ViewTab : ITab where TView : ContentControl where TViewModel : INotifyPropertyChanged
+ {
+ public string Title => getTitle(viewModel);
+ public ContentControl Content { get; }
+
+ public IEnumerable ToolbarCommands => viewModel is ITabToolbarCommands tabToolbarCommands
+ ? tabToolbarCommands.ToolbarCommands
+ : Enumerable.Empty();
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+
+ private readonly TViewModel viewModel;
+ private readonly Func getTitle;
+
+
+ public ViewTab(TView view, TViewModel viewModel, Expression> title)
+ {
+ if (title.Body is not MemberExpression titleMemberExpression)
+ throw new ArgumentException(@"Invalid expression type, expected viewModel => viewModel.TitlePropertyName", nameof(title));
+
+ var titlePropertyName = titleMemberExpression.Member.Name;
+
+ this.viewModel = viewModel;
+ getTitle = title.Compile();
+ Content = view;
+
+
+ viewModel.PropertyChanged += (_, args) =>
+ {
+ if (args.PropertyName == titlePropertyName)
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Title)));
+
+ else if (args.PropertyName == nameof(ToolbarCommands))
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ToolbarCommands)));
+ };
+ }
+
+
+ public void Activate()
+ {
+ (viewModel as ITabActivate)?.Activate();
+ }
+
+
+ public void Deactivate()
+ {
+ (viewModel as ITabActivate)?.Deactivate();
+ }
+ }
+}
diff --git a/PettingZoo/UI/Tab/ViewTabFactory.cs b/PettingZoo/UI/Tab/ViewTabFactory.cs
new file mode 100644
index 0000000..80d90dc
--- /dev/null
+++ b/PettingZoo/UI/Tab/ViewTabFactory.cs
@@ -0,0 +1,38 @@
+using System.Windows.Input;
+using PettingZoo.Core.Connection;
+using PettingZoo.UI.Tab.Publisher;
+using PettingZoo.UI.Tab.Subscriber;
+
+namespace PettingZoo.UI.Tab
+{
+ public class ViewTabFactory : ITabFactory
+ {
+ private readonly ITabHost tabHost;
+
+
+ public ViewTabFactory(ITabHost tabHost)
+ {
+ this.tabHost = tabHost;
+ }
+
+
+ public ITab CreateSubscriberTab(IConnection connection, ISubscriber subscriber)
+ {
+ var viewModel = new SubscriberViewModel(tabHost, this, connection, subscriber);
+ return new ViewTab(
+ new SubscriberView(viewModel),
+ viewModel,
+ vm => vm.Title);
+ }
+
+
+ public ITab CreatePublisherTab(IConnection connection, ReceivedMessageInfo? fromReceivedMessage = null)
+ {
+ var viewModel = new PublisherViewModel(tabHost, this, connection, fromReceivedMessage);
+ return new ViewTab(
+ new PublisherView(viewModel),
+ viewModel,
+ vm => vm.Title);
+ }
+ }
+}
diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs
deleted file mode 100644
index 0eebe89..0000000
--- a/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using System.Reflection;
-using System.Runtime.InteropServices;
-using System.Windows;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("Petting Zoo")]
-[assembly: AssemblyDescription("Petting Zoo - a RabbitMQ message viewer")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("X²Software")]
-[assembly: AssemblyProduct("Petting Zoo")]
-[assembly: AssemblyCopyright("Copyright © 2016")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-
-//In order to begin building localizable applications, set
-//CultureYouAreCodingWith in your .csproj file
-//inside a . For example, if you are using US english
-//in your source files, set the to en-US. Then uncomment
-//the NeutralResourceLanguage attribute below. Update the "en-US" in
-//the line below to match the UICulture setting in the project file.
-
-//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
-
-
-[assembly: ThemeInfo(
- ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
- //(used if a resource is not found in the page,
- // or application resource dictionaries)
- ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
- //(used if a resource is not found in the page,
- // app, or any theme specific resource dictionaries)
-)]
-
-
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-// You can specify all the values or you can default the Build and Revision Numbers
-// by using the '*' as shown below:
-// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
-[assembly: GuidAttribute("1739c968-ca2a-4293-b487-bfa193a7caf3")]
diff --git a/View/ConnectionWindow.xaml b/View/ConnectionWindow.xaml
deleted file mode 100644
index 700f928..0000000
--- a/View/ConnectionWindow.xaml
+++ /dev/null
@@ -1,62 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/View/ConnectionWindow.xaml.cs b/View/ConnectionWindow.xaml.cs
deleted file mode 100644
index d0e94a0..0000000
--- a/View/ConnectionWindow.xaml.cs
+++ /dev/null
@@ -1,72 +0,0 @@
-using System.Windows;
-using System.Windows.Input;
-using AutoMapper;
-using PettingZoo.Model;
-using PettingZoo.ViewModel;
-
-namespace PettingZoo.View
-{
- public class WindowConnectionInfoBuilder : IConnectionInfoBuilder
- {
- private readonly UserSettings userSettings;
-
- private static readonly IMapper ConnectionInfoMapper = new MapperConfiguration(cfg =>
- {
- cfg.RecognizeDestinationPrefixes("Last");
- cfg.RecognizePrefixes("Last");
-
- cfg.CreateMap().ReverseMap();
- }).CreateMapper();
-
-
- public WindowConnectionInfoBuilder(UserSettings userSettings)
- {
- this.userSettings = userSettings;
- }
-
-
- public ConnectionInfo Build()
- {
- var connectionInfo = ConnectionInfoMapper.Map(userSettings.ConnectionWindow);
- var viewModel = new ConnectionViewModel(connectionInfo);
-
- var dialog = new ConnectionWindow(viewModel)
- {
- Owner = Application.Current.MainWindow
- };
-
- viewModel.CloseWindow += (sender, args) =>
- {
- dialog.DialogResult = true;
- };
-
- connectionInfo = dialog.ShowDialog().GetValueOrDefault() ? viewModel.ToModel() : null;
- if (connectionInfo != null)
- {
- ConnectionInfoMapper.Map(connectionInfo, userSettings.ConnectionWindow);
- userSettings.Save();
- }
-
- return connectionInfo;
- }
- }
-
-
- public partial class ConnectionWindow
- {
- public ConnectionWindow(ConnectionViewModel viewModel)
- {
- WindowStartupLocation = WindowStartupLocation.CenterOwner;
-
- InitializeComponent();
- DataContext = viewModel;
- }
-
-
- private void NumericPreviewTextInput(object sender, TextCompositionEventArgs args)
- {
- if (!char.IsDigit(args.Text, args.Text.Length - 1))
- args.Handled = true;
- }
- }
-}
diff --git a/View/MainWindow.xaml b/View/MainWindow.xaml
deleted file mode 100644
index 1f907a8..0000000
--- a/View/MainWindow.xaml
+++ /dev/null
@@ -1,100 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/View/MainWindow.xaml.cs b/View/MainWindow.xaml.cs
deleted file mode 100644
index 9cce7cd..0000000
--- a/View/MainWindow.xaml.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using System;
-using System.Windows;
-using PettingZoo.ViewModel;
-
-namespace PettingZoo.View
-{
- public partial class MainWindow
- {
- public MainWindow(MainViewModel viewModel)
- {
- WindowStartupLocation = WindowStartupLocation.CenterScreen;
-
- InitializeComponent();
- DataContext = viewModel;
-
- Dispatcher.ShutdownStarted += OnDispatcherShutDownStarted;
- }
-
-
- private void OnDispatcherShutDownStarted(object sender, EventArgs e)
- {
- var disposable = DataContext as IDisposable;
- if (!ReferenceEquals(null, disposable))
- disposable.Dispose();
- }
- }
-}
diff --git a/ViewModel/ConnectionViewModel.cs b/ViewModel/ConnectionViewModel.cs
deleted file mode 100644
index 367eb50..0000000
--- a/ViewModel/ConnectionViewModel.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-using System;
-using System.Windows.Input;
-using AutoMapper;
-using PettingZoo.Infrastructure;
-using PettingZoo.Model;
-
-namespace PettingZoo.ViewModel
-{
- public class ConnectionViewModel : BaseViewModel
- {
- private static readonly IMapper ModelMapper = new MapperConfiguration(cfg =>
- cfg.CreateMap().ReverseMap()
- ).CreateMapper();
-
-
- private readonly DelegateCommand okCommand;
-
-
- public string Host { get; set; }
- public string VirtualHost { get; set; }
- public int Port { get; set; }
- public string Username { get; set; }
- public string Password { get; set; }
-
- public string Exchange { get; set; }
- public string RoutingKey { get; set; }
-
-
- public ICommand OkCommand { get { return okCommand; } }
-
- public event EventHandler CloseWindow;
-
-
- public ConnectionViewModel()
- {
- okCommand = new DelegateCommand(OkExecute, OkCanExecute);
- }
-
-
- public ConnectionViewModel(ConnectionInfo model) : this()
- {
- ModelMapper.Map(model, this);
- }
-
-
- public ConnectionInfo ToModel()
- {
- return ModelMapper.Map(this);
- }
-
-
- private void OkExecute()
- {
- if (CloseWindow != null)
- CloseWindow(this, EventArgs.Empty);
- }
-
-
- private bool OkCanExecute()
- {
- return true;
- }
- }
-}
diff --git a/ViewModel/MainViewModel.cs b/ViewModel/MainViewModel.cs
deleted file mode 100644
index e392b36..0000000
--- a/ViewModel/MainViewModel.cs
+++ /dev/null
@@ -1,210 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Threading;
-using System.Threading.Tasks;
-using System.Windows.Input;
-using PettingZoo.Infrastructure;
-using PettingZoo.Model;
-using PettingZoo.Properties;
-
-namespace PettingZoo.ViewModel
-{
- public class MainViewModel : BaseViewModel, IDisposable
- {
- private readonly TaskScheduler uiScheduler;
- private readonly IConnectionInfoBuilder connectionInfoBuilder;
- private readonly IConnectionFactory connectionFactory;
-
- private ConnectionInfo connectionInfo;
- private IConnection connection;
- private string connectionStatus;
- private readonly ObservableCollection messages;
- private MessageInfo selectedMessage;
-
- private readonly DelegateCommand connectCommand;
- private readonly DelegateCommand disconnectCommand;
- private readonly DelegateCommand clearCommand;
-
-
- public ConnectionInfo ConnectionInfo {
- get { return connectionInfo; }
- private set
- {
- if (value == connectionInfo)
- return;
-
- connectionInfo = value;
- RaisePropertyChanged();
- }
- }
-
- public string ConnectionStatus
- {
- get { return connectionStatus; }
- private set
- {
- if (value == connectionStatus)
- return;
-
- connectionStatus = value;
- RaisePropertyChanged();
- }
- }
-
- public ObservableCollection Messages { get { return messages; } }
-
- public MessageInfo SelectedMessage
- {
- get { return selectedMessage; }
- set
- {
- if (value == selectedMessage)
- return;
-
- selectedMessage = value;
- RaisePropertyChanged();
- RaiseOtherPropertyChanged("SelectedMessageBody");
- RaiseOtherPropertyChanged("SelectedMessageProperties");
- }
- }
-
- public string SelectedMessageBody
- {
- get
- {
- return SelectedMessage != null
- ? MessageBodyRenderer.Render(SelectedMessage.Body, SelectedMessage.ContentType)
- : "";
- }
- }
-
- public Dictionary SelectedMessageProperties
- {
- get { return SelectedMessage != null ? SelectedMessage.Properties : null; }
- }
-
- public ICommand ConnectCommand { get { return connectCommand; } }
- public ICommand DisconnectCommand { get { return disconnectCommand; } }
- public ICommand ClearCommand { get { return clearCommand; } }
-
-
- public MainViewModel(IConnectionInfoBuilder connectionInfoBuilder, IConnectionFactory connectionFactory)
- {
- uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
-
- this.connectionInfoBuilder = connectionInfoBuilder;
- this.connectionFactory = connectionFactory;
-
- connectionStatus = GetConnectionStatus(null);
- messages = new ObservableCollection();
-
- connectCommand = new DelegateCommand(ConnectExecute);
- disconnectCommand = new DelegateCommand(DisconnectExecute, DisconnectCanExecute);
- clearCommand = new DelegateCommand(ClearExecute, ClearCanExecute);
- }
-
-
- public void Dispose()
- {
- if (connection != null)
- {
- connection.Dispose();
- connection = null;
- }
- }
-
-
- private void ConnectExecute()
- {
- var newInfo = connectionInfoBuilder.Build();
- if (newInfo == null)
- return;
-
- if (connection != null)
- connection.Dispose();
-
- ConnectionInfo = newInfo;
- connection = connectionFactory.CreateConnection(connectionInfo);
- connection.MessageReceived += ConnectionMessageReceived;
- connection.StatusChanged += ConnectionStatusChanged;
-
- disconnectCommand.RaiseCanExecuteChanged();
- }
-
-
- private void DisconnectExecute()
- {
- if (connection != null)
- {
- connection.Dispose();
- connection = null;
- }
-
- ConnectionInfo = null;
- ConnectionStatus = GetConnectionStatus(null);
-
- disconnectCommand.RaiseCanExecuteChanged();
- }
-
-
- private bool DisconnectCanExecute()
- {
- return connection != null;
- }
-
-
- private void ClearExecute()
- {
- messages.Clear();
- clearCommand.RaiseCanExecuteChanged();
- }
-
-
- private bool ClearCanExecute()
- {
- return messages.Count > 0;
- }
-
-
- private void ConnectionStatusChanged(object sender, StatusChangedEventArgs args)
- {
- ConnectionStatus = GetConnectionStatus(args);
- }
-
-
- private void ConnectionMessageReceived(object sender, MessageReceivedEventArgs args)
- {
- RunFromUiScheduler(() =>
- {
- messages.Add(args.MessageInfo);
- clearCommand.RaiseCanExecuteChanged();
- });
- }
-
-
- private string GetConnectionStatus(StatusChangedEventArgs args)
- {
- if (args != null)
- switch (args.Status)
- {
- case Model.ConnectionStatus.Connecting:
- return String.Format(Resources.StatusConnecting, args.Context);
-
- case Model.ConnectionStatus.Connected:
- return String.Format(Resources.StatusConnected, args.Context);
-
- case Model.ConnectionStatus.Error:
- return String.Format(Resources.StatusError, args.Context);
- }
-
- return Resources.StatusDisconnected;
- }
-
-
- private void RunFromUiScheduler(Action action)
- {
- Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.None, uiScheduler);
- }
- }
-}
\ No newline at end of file
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000..4d5365b
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,39 @@
+image: Visual Studio 2022
+
+
+install:
+ - choco install gitversion.portable -pre -y
+
+before_build:
+ - nuget restore
+ - ps: gitversion /l console /output buildserver
+ - ps: build\UpdateVersion.ps1
+
+after_build:
+ # Create PettingZoo release
+ - cmd: dotnet publish -c Release -r win-x64 --self-contained=true -o publish\x64\selfcontained PettingZoo\PettingZoo.csproj
+ - cmd: dotnet publish -c Release -r win-x64 --self-contained=false -o publish\x64 PettingZoo\PettingZoo.csproj
+ - cmd: copy publish\x64\selfcontained\PettingZoo.exe publish\x64
+ - cmd: rmdir /s /q publish\x64\selfcontained
+ - cmd: 7z a output\PettingZoo-x64-%GitVersion_NuGetVersion%.zip %APPVEYOR_BUILD_FOLDER%\publish\x64\*
+ # Push artifacts
+ - ps: Get-ChildItem output\*.zip | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name }
+
+build:
+ project: PettingZoo.sln
+
+platform:
+ - Any CPU
+
+configuration:
+ - Release
+
+deploy:
+ - provider: GitHub
+ auth_token:
+ secure: dWOConKg3VTPvd9DmWOOKiX1SJCalaqKInuk9GlKQOZX2s+Bia49J7q+AHO8wFj7
+ artifact: /PettingZoo-.*\.zip/
+ draft: false
+ prerelease: false
+ on:
+ APPVEYOR_REPO_TAG: true
\ No newline at end of file
diff --git a/build/UpdateVersion.ps1 b/build/UpdateVersion.ps1
new file mode 100644
index 0000000..2a35611
--- /dev/null
+++ b/build/UpdateVersion.ps1
@@ -0,0 +1,38 @@
+# For debugging purposes
+if (-not (Test-Path env:APPVEYOR_BUILD_FOLDER))
+{
+ Write-Host "Warning: APPVEYOR_BUILD_FOLDER environment variable not set"
+ $env:APPVEYOR_BUILD_FOLDER = "P:\PettingZoo"
+}
+
+if (-not (Test-Path env:GitVersion_MajorMinorPatch))
+{
+ Write-Host "Warning: GitVersion_MajorMinorPatch environment variable not set"
+ $env:GitVersion_MajorMinorPatch = "0.0.1"
+}
+
+if (-not (Test-Path env:GitVersion_CommitsSinceVersionSource))
+{
+ Write-Host "Warning: GitVersion_CommitsSinceVersionSource environment variable not set"
+ $env:GitVersion_CommitsSinceVersionSource = "42"
+}
+
+$version = "$($env:GitVersion_MajorMinorPatch).$($env:GitVersion_CommitsSinceVersionSource)"
+
+Write-Host "Updating version to $($version) for projects in $($env:APPVEYOR_BUILD_FOLDER)"
+
+$projectFiles = Get-ChildItem $env:APPVEYOR_BUILD_FOLDER -Recurse *.csproj | Select -ExpandProperty FullName
+foreach ($projectFile in $projectFiles)
+{
+ $contents = Get-Content -Path $projectFile
+ if ($contents -match "(.+?)")
+ {
+ $contents = $contents -replace "(.+?)", "$($version)"
+ Set-Content -Path $projectFile -Value $contents
+ Write-Host "Updated $($projectFile)"
+ }
+ else
+ {
+ Write-Host "No version information in $($projectFile)"
+ }
+}
\ No newline at end of file
diff --git a/packages.config b/packages.config
deleted file mode 100644
index 7458215..0000000
--- a/packages.config
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file