From 2755e934404aa4bb51a5dbe23cb04e75abd6601a Mon Sep 17 00:00:00 2001 From: Mark van Renswoude Date: Sun, 28 Nov 2021 14:18:21 +0100 Subject: [PATCH 01/16] Started major refactoring - Upgrade to .NET 5 - Split functionality into libraries - Tabbed interface for multiple subscribers - Placeholder for publishing tab --- .editorconfig | 11 + .gitignore | 190 +------ App.config | 6 - Icons.xaml | 483 ------------------ Images/Clear.xaml | 60 --- Images/Connect.xaml | 203 -------- Images/Disconnect.xaml | 203 -------- Infrastructure/BaseViewModel.cs | 25 - Infrastructure/DelegateCommand.cs | 81 --- Model/ConnectionInfo.cs | 14 - Model/IConnection.cs | 46 -- Model/IConnectionFactory.cs | 7 - Model/IConnectionInfoBuilder.cs | 7 - Model/MessageInfo.cs | 31 -- Model/RabbitMQClientConnection.cs | 190 ------- Model/RabbitMQClientConnectionFactory.cs | 10 - .../Connection/ConnectionParams.cs | 21 + PettingZoo.Core/Connection/IConnection.cs | 36 ++ .../Connection/IConnectionFactory.cs | 7 + PettingZoo.Core/Connection/ISubscriber.cs | 26 + PettingZoo.Core/Connection/MessageInfo.cs | 23 + PettingZoo.Core/PettingZoo.Core.csproj | 12 + .../Rendering}/MessageBodyRenderer.cs | 12 +- .../PettingZoo.RabbitMQ.csproj | 17 + .../RabbitMQClientConnection.cs | 142 +++++ .../RabbitMQClientConnectionFactory.cs | 12 + .../RabbitMQClientPropertiesConverter.cs | 138 +++++ .../RabbitMQClientSubscriber.cs | 79 +++ .../RabbitMQProperties.cs | 4 +- .../RabbitMQPropertiesExtensions.cs | 14 + PettingZoo.csproj | 188 ------- PettingZoo.sln | 26 +- App.xaml => PettingZoo/App.xaml | 4 +- PettingZoo/App.xaml.cs | 13 + {Images => PettingZoo/Images}/Clear.svg | 0 {Images => PettingZoo/Images}/Connect.svg | 0 {Images => PettingZoo/Images}/Disconnect.svg | 0 .../Images}/PettingZoo-48.png | Bin {Images => PettingZoo/Images}/PettingZoo.ai | 0 {Images => PettingZoo/Images}/PettingZoo.ico | Bin PettingZoo/Images/Publish.svg | 63 +++ PettingZoo/Images/PublishSend.svg | 49 ++ PettingZoo/Images/Subscribe.svg | 63 +++ PettingZoo/PettingZoo.csproj | 112 ++++ PettingZoo.ico => PettingZoo/PettingZoo.ico | Bin App.xaml.cs => PettingZoo/Program.cs | 73 ++- .../Settings}/UserSettings.cs | 7 +- Style.xaml => PettingZoo/Style.xaml | 8 +- PettingZoo/UI/BaseViewModel.cs | 41 ++ .../UI/Connection/ConnectionViewModel.cs | 117 +++++ .../UI/Connection}/ConnectionWindow.xaml | 46 +- .../UI/Connection/ConnectionWindow.xaml.cs | 45 ++ .../ConnectionWindowStrings.Designer.cs | 162 ++++++ .../Connection/ConnectionWindowStrings.resx | 153 ++++++ PettingZoo/UI/Connection/IConnectionDialog.cs | 43 ++ PettingZoo/UI/DelegateCommand.cs | 79 +++ .../UI}/GridLayout.cs | 64 +-- .../UI}/ListBoxAutoScroll.cs | 47 +- PettingZoo/UI/Main/MainWindow.xaml | 97 ++++ PettingZoo/UI/Main/MainWindow.xaml.cs | 50 ++ .../UI/Main/MainWindowStrings.Designer.cs | 142 ++--- .../UI/Main/MainWindowStrings.resx | 53 +- PettingZoo/UI/Main/MainWindowViewModel.cs | 237 +++++++++ .../UI}/PasswordBoxAssistant.cs | 17 +- PettingZoo/UI/Subscribe/ISubscribeDialog.cs | 26 + PettingZoo/UI/Subscribe/SubscribeViewModel.cs | 66 +++ PettingZoo/UI/Subscribe/SubscribeWindow.xaml | 41 ++ .../UI/Subscribe/SubscribeWindow.xaml.cs | 37 ++ .../SubscribeWindowStrings.Designer.cs | 108 ++++ .../UI/Subscribe/SubscribeWindowStrings.resx | 135 +++++ PettingZoo/UI/SvgIconHelper.cs | 37 ++ PettingZoo/UI/Tab/ITab.cs | 37 ++ PettingZoo/UI/Tab/ITabFactory.cs | 15 + .../UI/Tab/Publisher/PublisherView.xaml | 21 + .../UI/Tab/Publisher/PublisherView.xaml.cs | 19 + .../UI/Tab/Publisher/PublisherViewModel.cs | 90 ++++ .../PublisherViewStrings.Designer.cs | 99 ++++ .../Tab/Publisher/PublisherViewStrings.resx | 132 +++++ .../UI/Tab/Subscriber/SubscriberView.xaml | 69 +++ .../UI/Tab/Subscriber/SubscriberView.xaml.cs | 21 + .../UI/Tab/Subscriber/SubscriberViewModel.cs | 131 +++++ .../SubscriberViewStrings.Designer.cs | 126 +++++ .../Tab/Subscriber/SubscriberViewStrings.resx | 141 +++++ PettingZoo/UI/Tab/ViewTab.cs | 51 ++ PettingZoo/UI/Tab/ViewTabFactory.cs | 31 ++ Properties/AssemblyInfo.cs | 54 -- View/ConnectionWindow.xaml.cs | 72 --- View/MainWindow.xaml | 100 ---- View/MainWindow.xaml.cs | 27 - ViewModel/ConnectionViewModel.cs | 64 --- ViewModel/MainViewModel.cs | 210 -------- packages.config | 7 - 92 files changed, 3520 insertions(+), 2556 deletions(-) create mode 100644 .editorconfig delete mode 100644 App.config delete mode 100644 Icons.xaml delete mode 100644 Images/Clear.xaml delete mode 100644 Images/Connect.xaml delete mode 100644 Images/Disconnect.xaml delete mode 100644 Infrastructure/BaseViewModel.cs delete mode 100644 Infrastructure/DelegateCommand.cs delete mode 100644 Model/ConnectionInfo.cs delete mode 100644 Model/IConnection.cs delete mode 100644 Model/IConnectionFactory.cs delete mode 100644 Model/IConnectionInfoBuilder.cs delete mode 100644 Model/MessageInfo.cs delete mode 100644 Model/RabbitMQClientConnection.cs delete mode 100644 Model/RabbitMQClientConnectionFactory.cs create mode 100644 PettingZoo.Core/Connection/ConnectionParams.cs create mode 100644 PettingZoo.Core/Connection/IConnection.cs create mode 100644 PettingZoo.Core/Connection/IConnectionFactory.cs create mode 100644 PettingZoo.Core/Connection/ISubscriber.cs create mode 100644 PettingZoo.Core/Connection/MessageInfo.cs create mode 100644 PettingZoo.Core/PettingZoo.Core.csproj rename {Model => PettingZoo.Core/Rendering}/MessageBodyRenderer.cs (73%) create mode 100644 PettingZoo.RabbitMQ/PettingZoo.RabbitMQ.csproj create mode 100644 PettingZoo.RabbitMQ/RabbitMQClientConnection.cs create mode 100644 PettingZoo.RabbitMQ/RabbitMQClientConnectionFactory.cs create mode 100644 PettingZoo.RabbitMQ/RabbitMQClientPropertiesConverter.cs create mode 100644 PettingZoo.RabbitMQ/RabbitMQClientSubscriber.cs rename {Model => PettingZoo.RabbitMQ}/RabbitMQProperties.cs (90%) create mode 100644 PettingZoo.RabbitMQ/RabbitMQPropertiesExtensions.cs delete mode 100644 PettingZoo.csproj rename App.xaml => PettingZoo/App.xaml (79%) create mode 100644 PettingZoo/App.xaml.cs rename {Images => PettingZoo/Images}/Clear.svg (100%) rename {Images => PettingZoo/Images}/Connect.svg (100%) rename {Images => PettingZoo/Images}/Disconnect.svg (100%) rename {Images => PettingZoo/Images}/PettingZoo-48.png (100%) rename {Images => PettingZoo/Images}/PettingZoo.ai (100%) rename {Images => PettingZoo/Images}/PettingZoo.ico (100%) create mode 100644 PettingZoo/Images/Publish.svg create mode 100644 PettingZoo/Images/PublishSend.svg create mode 100644 PettingZoo/Images/Subscribe.svg create mode 100644 PettingZoo/PettingZoo.csproj rename PettingZoo.ico => PettingZoo/PettingZoo.ico (100%) rename App.xaml.cs => PettingZoo/Program.cs (51%) rename {Model => PettingZoo/Settings}/UserSettings.cs (91%) rename Style.xaml => PettingZoo/Style.xaml (89%) create mode 100644 PettingZoo/UI/BaseViewModel.cs create mode 100644 PettingZoo/UI/Connection/ConnectionViewModel.cs rename {View => PettingZoo/UI/Connection}/ConnectionWindow.xaml (57%) create mode 100644 PettingZoo/UI/Connection/ConnectionWindow.xaml.cs create mode 100644 PettingZoo/UI/Connection/ConnectionWindowStrings.Designer.cs create mode 100644 PettingZoo/UI/Connection/ConnectionWindowStrings.resx create mode 100644 PettingZoo/UI/Connection/IConnectionDialog.cs create mode 100644 PettingZoo/UI/DelegateCommand.cs rename {Infrastructure => PettingZoo/UI}/GridLayout.cs (67%) rename {Infrastructure => PettingZoo/UI}/ListBoxAutoScroll.cs (75%) create mode 100644 PettingZoo/UI/Main/MainWindow.xaml create mode 100644 PettingZoo/UI/Main/MainWindow.xaml.cs rename Properties/Resources.Designer.cs => PettingZoo/UI/Main/MainWindowStrings.Designer.cs (55%) rename Properties/Resources.resx => PettingZoo/UI/Main/MainWindowStrings.resx (83%) create mode 100644 PettingZoo/UI/Main/MainWindowViewModel.cs rename {Infrastructure => PettingZoo/UI}/PasswordBoxAssistant.cs (92%) create mode 100644 PettingZoo/UI/Subscribe/ISubscribeDialog.cs create mode 100644 PettingZoo/UI/Subscribe/SubscribeViewModel.cs create mode 100644 PettingZoo/UI/Subscribe/SubscribeWindow.xaml create mode 100644 PettingZoo/UI/Subscribe/SubscribeWindow.xaml.cs create mode 100644 PettingZoo/UI/Subscribe/SubscribeWindowStrings.Designer.cs create mode 100644 PettingZoo/UI/Subscribe/SubscribeWindowStrings.resx create mode 100644 PettingZoo/UI/SvgIconHelper.cs create mode 100644 PettingZoo/UI/Tab/ITab.cs create mode 100644 PettingZoo/UI/Tab/ITabFactory.cs create mode 100644 PettingZoo/UI/Tab/Publisher/PublisherView.xaml create mode 100644 PettingZoo/UI/Tab/Publisher/PublisherView.xaml.cs create mode 100644 PettingZoo/UI/Tab/Publisher/PublisherViewModel.cs create mode 100644 PettingZoo/UI/Tab/Publisher/PublisherViewStrings.Designer.cs create mode 100644 PettingZoo/UI/Tab/Publisher/PublisherViewStrings.resx create mode 100644 PettingZoo/UI/Tab/Subscriber/SubscriberView.xaml create mode 100644 PettingZoo/UI/Tab/Subscriber/SubscriberView.xaml.cs create mode 100644 PettingZoo/UI/Tab/Subscriber/SubscriberViewModel.cs create mode 100644 PettingZoo/UI/Tab/Subscriber/SubscriberViewStrings.Designer.cs create mode 100644 PettingZoo/UI/Tab/Subscriber/SubscriberViewStrings.resx create mode 100644 PettingZoo/UI/Tab/ViewTab.cs create mode 100644 PettingZoo/UI/Tab/ViewTabFactory.cs delete mode 100644 Properties/AssemblyInfo.cs delete mode 100644 View/ConnectionWindow.xaml.cs delete mode 100644 View/MainWindow.xaml delete mode 100644 View/MainWindow.xaml.cs delete mode 100644 ViewModel/ConnectionViewModel.cs delete mode 100644 ViewModel/MainViewModel.cs delete mode 100644 packages.config diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0eeeaa4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +[*.cs] + +# IDE0011: Add braces +csharp_prefer_braces = when_multiline +csharp_style_var_for_built_in_types=true:silent +csharp_style_var_when_type_is_apparent=true:silent +csharp_style_var_elsewhere=true:silent + +dotnet_diagnostic.IDE0055.severity = none + +dotnet_diagnostic.IDE0130.severity = none \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7964536..75ac72d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,189 +1,5 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo +.VS *.user -*.sln.docstates -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -x64/ -build/ -bld/ -[Bb]in/ -[Oo]bj/ - -# Roslyn cache directories -*.ide/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -#NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opensdf -*.sdf -*.cachefile - -# Visual Studio profiler -*.psess -*.vsp -*.vspx - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding addin-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -## TODO: Comment the next line if you want to checkin your -## web deploy settings but do note that will include unencrypted -## passwords -#*.pubxml - -# NuGet Packages Directory -packages/* -## TODO: If the tool you use requires repositories.config -## uncomment the next line -#!packages/repositories.config - -# Enable "build/" folder in the NuGet Packages folder since -# NuGet packages use it for MSBuild targets. -# This line needs to be after the ignore of the build folder -# (and the packages folder if the line above has been uncommented) -!packages/build/ - -# Windows Azure Build Output -csx/ -*.build.csdef - -# Windows Store app package directory -AppPackages/ - -# Others -sql/ -*.Cache -ClientBin/ -[Ss]tyle[Cc]op.* -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.pfx -*.publishsettings -node_modules/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# LightSwitch generated files -GeneratedArtifacts/ -_Pvt_Extensions/ -ModelManifest.xml \ No newline at end of file +bin +obj \ No newline at end of file diff --git a/App.config b/App.config deleted file mode 100644 index 8e15646..0000000 --- a/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Icons.xaml b/Icons.xaml deleted file mode 100644 index 848e067..0000000 --- a/Icons.xaml +++ /dev/null @@ -1,483 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Images/Clear.xaml b/Images/Clear.xaml deleted file mode 100644 index 85ad8ee..0000000 --- a/Images/Clear.xaml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Images/Connect.xaml b/Images/Connect.xaml deleted file mode 100644 index a31ac6f..0000000 --- a/Images/Connect.xaml +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Images/Disconnect.xaml b/Images/Disconnect.xaml deleted file mode 100644 index 66d8822..0000000 --- a/Images/Disconnect.xaml +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Infrastructure/BaseViewModel.cs b/Infrastructure/BaseViewModel.cs deleted file mode 100644 index 152d71b..0000000 --- a/Infrastructure/BaseViewModel.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.ComponentModel; -using System.Runtime.CompilerServices; - -namespace PettingZoo.Infrastructure -{ - public class BaseViewModel : INotifyPropertyChanged - { - public event PropertyChangedEventHandler PropertyChanged; - - protected virtual void RaisePropertyChanged([CallerMemberName] string propertyName = null) - { - var handler = PropertyChanged; - if (handler != null) - handler(this, new PropertyChangedEventArgs(propertyName)); - } - - - protected virtual void RaiseOtherPropertyChanged(string propertyName) - { - var handler = PropertyChanged; - if (handler != null) - handler(this, new PropertyChangedEventArgs(propertyName)); - } - } -} diff --git a/Infrastructure/DelegateCommand.cs b/Infrastructure/DelegateCommand.cs deleted file mode 100644 index f4eb492..0000000 --- a/Infrastructure/DelegateCommand.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Windows.Input; - -namespace PettingZoo.Infrastructure -{ - public class DelegateCommand : ICommand - { - private readonly Func canExecute; - private readonly Action execute; - - public event EventHandler CanExecuteChanged; - - - public DelegateCommand(Action execute) : this(execute, null) - { - } - - public DelegateCommand(Action execute, Func canExecute) - { - this.execute = execute; - this.canExecute = canExecute; - } - - - public bool CanExecute(object parameter) - { - return canExecute == null || canExecute((T)parameter); - } - - - public void Execute(object parameter) - { - execute((T)parameter); - } - - - public void RaiseCanExecuteChanged() - { - if (CanExecuteChanged != null) - CanExecuteChanged(this, EventArgs.Empty); - } - } - - - - public class DelegateCommand : ICommand - { - private readonly Func canExecute; - private readonly Action execute; - - public event EventHandler CanExecuteChanged; - - - public DelegateCommand(Action execute) : this(execute, null) { } - - public DelegateCommand(Action execute, Func canExecute) - { - this.execute = execute; - this.canExecute = canExecute; - } - - - public bool CanExecute(object parameter) - { - return canExecute == null || canExecute(); - } - - - public void Execute(object parameter) - { - execute(); - } - - - public void RaiseCanExecuteChanged() - { - if (CanExecuteChanged != null) - CanExecuteChanged(this, EventArgs.Empty); - } - } -} diff --git a/Model/ConnectionInfo.cs b/Model/ConnectionInfo.cs deleted file mode 100644 index 8ba3ba4..0000000 --- a/Model/ConnectionInfo.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace PettingZoo.Model -{ - public class ConnectionInfo - { - public string Host { get; set; } - public string VirtualHost { get; set; } - public int Port { get; set; } - public string Username { get; set; } - public string Password { get; set; } - - public string Exchange { get; set; } - public string RoutingKey { get; set; } - } -} diff --git a/Model/IConnection.cs b/Model/IConnection.cs deleted file mode 100644 index 394a721..0000000 --- a/Model/IConnection.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; - -namespace PettingZoo.Model -{ - public enum ConnectionStatus - { - Disconnected, - Connecting, - Connected, - Error - } - - - public class StatusChangedEventArgs : EventArgs - { - public ConnectionStatus Status { get; private set; } - public string Context { get; private set; } - - - public StatusChangedEventArgs(ConnectionStatus status, string context) - { - Status = status; - Context = context; - } - } - - - public class MessageReceivedEventArgs : EventArgs - { - public MessageInfo MessageInfo { get; private set; } - - - public MessageReceivedEventArgs(MessageInfo messageInfo) - { - MessageInfo = messageInfo; - } - } - - - - public interface IConnection : IDisposable - { - event EventHandler StatusChanged; - event EventHandler MessageReceived; - } -} diff --git a/Model/IConnectionFactory.cs b/Model/IConnectionFactory.cs deleted file mode 100644 index e1b6bd5..0000000 --- a/Model/IConnectionFactory.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace PettingZoo.Model -{ - public interface IConnectionFactory - { - IConnection CreateConnection(ConnectionInfo connectionInfo); - } -} diff --git a/Model/IConnectionInfoBuilder.cs b/Model/IConnectionInfoBuilder.cs deleted file mode 100644 index bdd683c..0000000 --- a/Model/IConnectionInfoBuilder.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace PettingZoo.Model -{ - public interface IConnectionInfoBuilder - { - ConnectionInfo Build(); - } -} diff --git a/Model/MessageInfo.cs b/Model/MessageInfo.cs deleted file mode 100644 index 7a29779..0000000 --- a/Model/MessageInfo.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace PettingZoo.Model -{ - public class MessageInfo - { - public DateTime Timestamp { get; set; } - public string Exchange { get; set; } - public string RoutingKey { get; set; } - public byte[] Body { get; set; } - - public Dictionary Properties; - - public string ContentType - { - get - { - return Properties != null && Properties.ContainsKey(RabbitMQProperties.ContentType) - ? Properties[RabbitMQProperties.ContentType] - : ""; - } - } - - - public MessageInfo() - { - Timestamp = DateTime.Now; - } - } -} diff --git a/Model/RabbitMQClientConnection.cs b/Model/RabbitMQClientConnection.cs deleted file mode 100644 index 0cd573d..0000000 --- a/Model/RabbitMQClientConnection.cs +++ /dev/null @@ -1,190 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using PettingZoo.Properties; -using RabbitMQ.Client; -using RabbitMQ.Client.Events; - -namespace PettingZoo.Model -{ - public class RabbitMQClientConnection : IConnection - { - private const int ConnectRetryDelay = 5000; - - private readonly CancellationTokenSource connectionTaskToken; - private RabbitMQ.Client.IConnection connection; - private IModel model; - - - public event EventHandler StatusChanged; - public event EventHandler MessageReceived; - - - public RabbitMQClientConnection(ConnectionInfo connectionInfo) - { - connectionTaskToken = new CancellationTokenSource(); - var connectionToken = connectionTaskToken.Token; - - Task.Factory.StartNew(() => TryConnection(connectionInfo, connectionToken), connectionToken); - } - - - public void Dispose() - { - connectionTaskToken.Cancel(); - - if (model != null) - { - model.Dispose(); - model = null; - } - - if (connection != null) - { - connection.Dispose(); - connection = null; - } - - StatusChanged = null; - MessageReceived = null; - } - - - private void TryConnection(ConnectionInfo connectionInfo, CancellationToken cancellationToken) - { - var factory = new ConnectionFactory - { - HostName = connectionInfo.Host, - Port = connectionInfo.Port, - VirtualHost = connectionInfo.VirtualHost, - UserName = connectionInfo.Username, - Password = connectionInfo.Password - }; - - var statusContext = String.Format("{0}:{1}{2}", connectionInfo.Host, connectionInfo.Port, connectionInfo.VirtualHost); - - while (!cancellationToken.IsCancellationRequested) - { - DoStatusChanged(ConnectionStatus.Connecting, statusContext); - try - { - connection = factory.CreateConnection(); - model = connection.CreateModel(); - - var queueName = model.QueueDeclare().QueueName; - model.QueueBind(queueName, connectionInfo.Exchange, connectionInfo.RoutingKey); - - - var consumer = new EventingBasicConsumer(model); - consumer.Received += ClientReceived; - - model.BasicConsume(queueName, true, consumer); - DoStatusChanged(ConnectionStatus.Connected, statusContext); - - break; - } - catch (Exception e) - { - DoStatusChanged(ConnectionStatus.Error, e.Message); - Task.Delay(ConnectRetryDelay, cancellationToken).Wait(cancellationToken); - } - } - } - - - private void ClientReceived(object sender, BasicDeliverEventArgs args) - { - if (MessageReceived == null) - return; - - MessageReceived(this, new MessageReceivedEventArgs( - new MessageInfo - { - Exchange = args.Exchange, - RoutingKey = args.RoutingKey, - Body = args.Body, - Properties = ConvertProperties(args.BasicProperties) - } - )); - } - - - private void DoStatusChanged(ConnectionStatus status, string context = null) - { - if (StatusChanged != null) - StatusChanged(this, new StatusChangedEventArgs(status, context)); - } - - - private static Dictionary ConvertProperties(IBasicProperties basicProperties) - { - var properties = new Dictionary(); - - if (basicProperties.IsDeliveryModePresent()) - { - string deliveryMode; - - switch (basicProperties.DeliveryMode) - { - case 1: - deliveryMode = Resources.DeliveryModeNonPersistent; - break; - - case 2: - deliveryMode = Resources.DeliveryModePersistent; - break; - - default: - deliveryMode = basicProperties.DeliveryMode.ToString(CultureInfo.InvariantCulture); - break; - } - - properties.Add(RabbitMQProperties.DeliveryMode, deliveryMode); - } - - if (basicProperties.IsContentTypePresent()) - properties.Add(RabbitMQProperties.ContentType, basicProperties.ContentType); - - if (basicProperties.IsContentEncodingPresent()) - properties.Add(RabbitMQProperties.ContentEncoding, basicProperties.ContentEncoding); - - if (basicProperties.IsPriorityPresent()) - properties.Add(RabbitMQProperties.Priority, basicProperties.Priority.ToString(CultureInfo.InvariantCulture)); - - if (basicProperties.IsCorrelationIdPresent()) - properties.Add(RabbitMQProperties.Priority, basicProperties.CorrelationId); - - if (basicProperties.IsReplyToPresent()) - properties.Add(RabbitMQProperties.ReplyTo, basicProperties.ReplyTo); - - if (basicProperties.IsExpirationPresent()) - properties.Add(RabbitMQProperties.Expiration, basicProperties.Expiration); - - if (basicProperties.IsMessageIdPresent()) - properties.Add(RabbitMQProperties.MessageId, basicProperties.MessageId); - - if (basicProperties.IsTimestampPresent()) - properties.Add(RabbitMQProperties.Timestamp, basicProperties.Timestamp.UnixTime.ToString(CultureInfo.InvariantCulture)); - - if (basicProperties.IsTypePresent()) - properties.Add(RabbitMQProperties.Type, basicProperties.Type); - - if (basicProperties.IsUserIdPresent()) - properties.Add(RabbitMQProperties.UserId, basicProperties.UserId); - - if (basicProperties.IsAppIdPresent()) - properties.Add(RabbitMQProperties.UserId, basicProperties.AppId); - - if (basicProperties.IsClusterIdPresent()) - properties.Add(RabbitMQProperties.ClusterId, basicProperties.ClusterId); - - foreach (var header in basicProperties.Headers) - properties.Add(header.Key, Encoding.UTF8.GetString((byte[])header.Value)); - - return properties; - } - } -} diff --git a/Model/RabbitMQClientConnectionFactory.cs b/Model/RabbitMQClientConnectionFactory.cs deleted file mode 100644 index d61aa3e..0000000 --- a/Model/RabbitMQClientConnectionFactory.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace PettingZoo.Model -{ - public class RabbitMQClientConnectionFactory : IConnectionFactory - { - public IConnection CreateConnection(ConnectionInfo connectionInfo) - { - return new RabbitMQClientConnection(connectionInfo); - } - } -} diff --git a/PettingZoo.Core/Connection/ConnectionParams.cs b/PettingZoo.Core/Connection/ConnectionParams.cs new file mode 100644 index 0000000..fb63f5e --- /dev/null +++ b/PettingZoo.Core/Connection/ConnectionParams.cs @@ -0,0 +1,21 @@ +namespace PettingZoo.Core.Connection +{ + public class ConnectionParams + { + public string Host { get; } + public string VirtualHost { get; } + public int Port { get; } + public string Username { get; } + public string Password { get; } + + + public ConnectionParams(string host, string virtualHost, int port, string username, string password) + { + Host = host; + VirtualHost = virtualHost; + Port = port; + Username = username; + Password = password; + } + } +} diff --git a/PettingZoo.Core/Connection/IConnection.cs b/PettingZoo.Core/Connection/IConnection.cs new file mode 100644 index 0000000..f29014c --- /dev/null +++ b/PettingZoo.Core/Connection/IConnection.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; + +namespace PettingZoo.Core.Connection +{ + public interface IConnection : IAsyncDisposable + { + event EventHandler StatusChanged; + + ISubscriber Subscribe(string exchange, string routingKey); + Task Publish(MessageInfo messageInfo); + } + + + public enum ConnectionStatus + { + Disconnected, + Connecting, + Connected, + Error + } + + + public class StatusChangedEventArgs : EventArgs + { + public ConnectionStatus Status { get; } + public string? Context { get; } + + + public StatusChangedEventArgs(ConnectionStatus status, string? context) + { + Status = status; + Context = context; + } + } +} diff --git a/PettingZoo.Core/Connection/IConnectionFactory.cs b/PettingZoo.Core/Connection/IConnectionFactory.cs new file mode 100644 index 0000000..a2f7170 --- /dev/null +++ b/PettingZoo.Core/Connection/IConnectionFactory.cs @@ -0,0 +1,7 @@ +namespace PettingZoo.Core.Connection +{ + public interface IConnectionFactory + { + IConnection CreateConnection(ConnectionParams connectionInfo); + } +} diff --git a/PettingZoo.Core/Connection/ISubscriber.cs b/PettingZoo.Core/Connection/ISubscriber.cs new file mode 100644 index 0000000..3ec09c8 --- /dev/null +++ b/PettingZoo.Core/Connection/ISubscriber.cs @@ -0,0 +1,26 @@ +using System; + +namespace PettingZoo.Core.Connection +{ + public interface ISubscriber : IAsyncDisposable + { + string Exchange {get; } + string RoutingKey { get; } + + event EventHandler? MessageReceived; + + void Start(); + } + + + public class MessageReceivedEventArgs : EventArgs + { + public MessageInfo MessageInfo { get; } + + + public MessageReceivedEventArgs(MessageInfo messageInfo) + { + MessageInfo = messageInfo; + } + } +} diff --git a/PettingZoo.Core/Connection/MessageInfo.cs b/PettingZoo.Core/Connection/MessageInfo.cs new file mode 100644 index 0000000..f70abd6 --- /dev/null +++ b/PettingZoo.Core/Connection/MessageInfo.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace PettingZoo.Core.Connection +{ + public class MessageInfo + { + public DateTime Timestamp { get; } + public string Exchange { get; } + public string RoutingKey { get; } + public byte[] Body { get; } + public IDictionary Properties { get; } + + public MessageInfo(string exchange, string routingKey, byte[] body, IDictionary properties, DateTime timestamp) + { + Exchange = exchange; + RoutingKey = routingKey; + Body = body; + Properties = properties; + Timestamp = timestamp; + } + } +} diff --git a/PettingZoo.Core/PettingZoo.Core.csproj b/PettingZoo.Core/PettingZoo.Core.csproj new file mode 100644 index 0000000..951e5aa --- /dev/null +++ b/PettingZoo.Core/PettingZoo.Core.csproj @@ -0,0 +1,12 @@ + + + + net5.0 + enable + + + + + + + diff --git a/Model/MessageBodyRenderer.cs b/PettingZoo.Core/Rendering/MessageBodyRenderer.cs similarity index 73% rename from Model/MessageBodyRenderer.cs rename to PettingZoo.Core/Rendering/MessageBodyRenderer.cs index 1ba2e62..acdc033 100644 --- a/Model/MessageBodyRenderer.cs +++ b/PettingZoo.Core/Rendering/MessageBodyRenderer.cs @@ -3,11 +3,11 @@ using System.Collections.Generic; using System.Text; using Newtonsoft.Json; -namespace PettingZoo.Model +namespace PettingZoo.Core.Rendering { public class MessageBodyRenderer { - public static Dictionary> ContentTypeHandlers = new Dictionary> + public static Dictionary> ContentTypeHandlers = new() { { "application/json", RenderJson } }; @@ -15,13 +15,11 @@ namespace PettingZoo.Model public static string Render(byte[] body, string contentType = "") { - Func handler; - - if (ContentTypeHandlers.TryGetValue(contentType, out handler)) - return handler(body); + return ContentTypeHandlers.TryGetValue(contentType, out var handler) + ? handler(body) + : Encoding.UTF8.GetString(body); // ToDo hex output if required - return Encoding.UTF8.GetString(body); } diff --git a/PettingZoo.RabbitMQ/PettingZoo.RabbitMQ.csproj b/PettingZoo.RabbitMQ/PettingZoo.RabbitMQ.csproj new file mode 100644 index 0000000..38e4451 --- /dev/null +++ b/PettingZoo.RabbitMQ/PettingZoo.RabbitMQ.csproj @@ -0,0 +1,17 @@ + + + + net5.0 + enable + + + + + + + + + + + + diff --git a/PettingZoo.RabbitMQ/RabbitMQClientConnection.cs b/PettingZoo.RabbitMQ/RabbitMQClientConnection.cs new file mode 100644 index 0000000..9682070 --- /dev/null +++ b/PettingZoo.RabbitMQ/RabbitMQClientConnection.cs @@ -0,0 +1,142 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PettingZoo.Core.Connection; +using RabbitMQ.Client; + +namespace PettingZoo.RabbitMQ +{ + public class RabbitMQClientConnection : Core.Connection.IConnection + { + private const int ConnectRetryDelay = 5000; + + private readonly CancellationTokenSource connectionTaskToken = new(); + private readonly Task connectionTask; + private readonly object connectionLock = new(); + private global::RabbitMQ.Client.IConnection? connection; + private IModel? model; + + + public event EventHandler? StatusChanged; + + + public RabbitMQClientConnection(ConnectionParams connectionParams) + { + connectionTask = Task.Factory.StartNew(() => TryConnection(connectionParams, connectionTaskToken.Token), CancellationToken.None); + } + + + public async ValueTask DisposeAsync() + { + connectionTaskToken.Cancel(); + if (!connectionTask.IsCompleted) + await connectionTask; + + lock (connectionLock) + { + if (model != null) + { + model.Dispose(); + model = null; + } + + if (connection != null) + { + connection.Dispose(); + connection = null; + } + } + } + + + public ISubscriber Subscribe(string exchange, string routingKey) + { + lock (connectionLock) + { + var subscriber = new RabbitMQClientSubscriber(model, exchange, routingKey); + if (model != null) + return subscriber; + + + void ConnectSubscriber(object? sender, StatusChangedEventArgs args) + { + if (args.Status != ConnectionStatus.Connected) + return; + + lock (connectionLock) + { + if (model == null) + return; + + subscriber.Connected(model); + } + + StatusChanged -= ConnectSubscriber; + } + + + StatusChanged += ConnectSubscriber; + return subscriber; + } + } + + + public Task Publish(MessageInfo messageInfo) + { + if (model == null) + throw new InvalidOperationException("Not connected"); + + model.BasicPublish(messageInfo.Exchange, messageInfo.RoutingKey, false, + RabbitMQClientPropertiesConverter.Convert(messageInfo.Properties, model.CreateBasicProperties()), + messageInfo.Body); + + return Task.CompletedTask; + } + + + private void TryConnection(ConnectionParams connectionParams, CancellationToken cancellationToken) + { + var factory = new ConnectionFactory + { + HostName = connectionParams.Host, + Port = connectionParams.Port, + VirtualHost = connectionParams.VirtualHost, + UserName = connectionParams.Username, + Password = connectionParams.Password + }; + + var statusContext = $"{connectionParams.Host}:{connectionParams.Port}{connectionParams.VirtualHost}"; + + while (!cancellationToken.IsCancellationRequested) + { + DoStatusChanged(ConnectionStatus.Connecting, statusContext); + try + { + connection = factory.CreateConnection(); + model = connection.CreateModel(); + + DoStatusChanged(ConnectionStatus.Connected, statusContext); + break; + } + catch (Exception e) + { + DoStatusChanged(ConnectionStatus.Error, e.Message); + + try + { + Task.Delay(ConnectRetryDelay, cancellationToken).Wait(cancellationToken); + } + catch (OperationCanceledException) + { + } + } + } + } + + + private void DoStatusChanged(ConnectionStatus status, string? context = null) + { + StatusChanged?.Invoke(this, new StatusChangedEventArgs(status, context)); + } + } +} diff --git a/PettingZoo.RabbitMQ/RabbitMQClientConnectionFactory.cs b/PettingZoo.RabbitMQ/RabbitMQClientConnectionFactory.cs new file mode 100644 index 0000000..4850b3f --- /dev/null +++ b/PettingZoo.RabbitMQ/RabbitMQClientConnectionFactory.cs @@ -0,0 +1,12 @@ +using PettingZoo.Core.Connection; + +namespace PettingZoo.RabbitMQ +{ + public class RabbitMQClientConnectionFactory : IConnectionFactory + { + public IConnection CreateConnection(ConnectionParams connectionParams) + { + return new RabbitMQClientConnection(connectionParams); + } + } +} diff --git a/PettingZoo.RabbitMQ/RabbitMQClientPropertiesConverter.cs b/PettingZoo.RabbitMQ/RabbitMQClientPropertiesConverter.cs new file mode 100644 index 0000000..967413f --- /dev/null +++ b/PettingZoo.RabbitMQ/RabbitMQClientPropertiesConverter.cs @@ -0,0 +1,138 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using RabbitMQ.Client; + +namespace PettingZoo.RabbitMQ +{ + public static class RabbitMQClientPropertiesConverter + { + public static IDictionary Convert(IBasicProperties basicProperties) + { + var properties = new Dictionary(); + + if (basicProperties.IsDeliveryModePresent()) + properties.Add(RabbitMQProperties.DeliveryMode, basicProperties.DeliveryMode.ToString(CultureInfo.InvariantCulture)); + + if (basicProperties.IsContentTypePresent()) + properties.Add(RabbitMQProperties.ContentType, basicProperties.ContentType); + + if (basicProperties.IsContentEncodingPresent()) + properties.Add(RabbitMQProperties.ContentEncoding, basicProperties.ContentEncoding); + + if (basicProperties.IsPriorityPresent()) + properties.Add(RabbitMQProperties.Priority, basicProperties.Priority.ToString(CultureInfo.InvariantCulture)); + + if (basicProperties.IsCorrelationIdPresent()) + properties.Add(RabbitMQProperties.Priority, basicProperties.CorrelationId); + + if (basicProperties.IsReplyToPresent()) + properties.Add(RabbitMQProperties.ReplyTo, basicProperties.ReplyTo); + + if (basicProperties.IsExpirationPresent()) + properties.Add(RabbitMQProperties.Expiration, basicProperties.Expiration); + + if (basicProperties.IsMessageIdPresent()) + properties.Add(RabbitMQProperties.MessageId, basicProperties.MessageId); + + if (basicProperties.IsTimestampPresent()) + properties.Add(RabbitMQProperties.Timestamp, basicProperties.Timestamp.UnixTime.ToString(CultureInfo.InvariantCulture)); + + if (basicProperties.IsTypePresent()) + properties.Add(RabbitMQProperties.Type, basicProperties.Type); + + if (basicProperties.IsUserIdPresent()) + properties.Add(RabbitMQProperties.UserId, basicProperties.UserId); + + if (basicProperties.IsAppIdPresent()) + properties.Add(RabbitMQProperties.UserId, basicProperties.AppId); + + if (basicProperties.IsClusterIdPresent()) + properties.Add(RabbitMQProperties.ClusterId, basicProperties.ClusterId); + + // ReSharper disable once InvertIf + if (basicProperties.Headers != null) + { + foreach (var (key, value) in basicProperties.Headers) + properties.Add(key, Encoding.UTF8.GetString((byte[]) value)); + } + + return properties; + } + + + public static IBasicProperties Convert(IDictionary properties, IBasicProperties targetProperties) + { + foreach (var (key, value) in properties) + { + switch (key) + { + case RabbitMQProperties.DeliveryMode: + if (byte.TryParse(value, out var deliveryMode)) + targetProperties.DeliveryMode = deliveryMode; + + break; + + case RabbitMQProperties.ContentType: + targetProperties.ContentType = value; + break; + + case RabbitMQProperties.ContentEncoding: + targetProperties.ContentEncoding = value; + break; + + case RabbitMQProperties.Priority: + if (byte.TryParse(value, out var priority)) + targetProperties.Priority = priority; + + break; + + case RabbitMQProperties.CorrelationId: + targetProperties.CorrelationId = value; + break; + + case RabbitMQProperties.ReplyTo: + targetProperties.ReplyTo = value; + break; + + case RabbitMQProperties.Expiration: + targetProperties.Expiration = value; + break; + + case RabbitMQProperties.MessageId: + targetProperties.MessageId = value; + break; + + case RabbitMQProperties.Timestamp: + if (long.TryParse(value, out var timestamp)) + targetProperties.Timestamp = new AmqpTimestamp(timestamp); + + break; + + case RabbitMQProperties.Type: + targetProperties.Type = value; + break; + + case RabbitMQProperties.UserId: + targetProperties.UserId = value; + break; + + case RabbitMQProperties.AppId: + targetProperties.AppId = value; + break; + + case RabbitMQProperties.ClusterId: + targetProperties.ClusterId = value; + break; + + default: + targetProperties.Headers ??= new Dictionary(); + targetProperties.Headers.Add(key, Encoding.UTF8.GetBytes(value)); + break; + } + } + + return targetProperties; + } + } +} diff --git a/PettingZoo.RabbitMQ/RabbitMQClientSubscriber.cs b/PettingZoo.RabbitMQ/RabbitMQClientSubscriber.cs new file mode 100644 index 0000000..7f7dd26 --- /dev/null +++ b/PettingZoo.RabbitMQ/RabbitMQClientSubscriber.cs @@ -0,0 +1,79 @@ +using System; +using System.Threading.Tasks; +using PettingZoo.Core.Connection; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace PettingZoo.RabbitMQ +{ + public class RabbitMQClientSubscriber : ISubscriber + { + private IModel? model; + + private string? consumerTag; + private bool started; + + public string Exchange { get; } + public string RoutingKey { get; } + public event EventHandler? MessageReceived; + + + public RabbitMQClientSubscriber(IModel? model, string exchange, string routingKey) + { + this.model = model; + Exchange = exchange; + RoutingKey = routingKey; + } + + + public ValueTask DisposeAsync() + { + if (model != null && consumerTag != null && model.IsOpen) + model.BasicCancelNoWait(consumerTag); + + return default; + } + + + public void Start() + { + started = true; + if (model == null) + return; + + var queueName = model.QueueDeclare().QueueName; + model.QueueBind(queueName, Exchange, RoutingKey); + + var consumer = new EventingBasicConsumer(model); + consumer.Received += ClientReceived; + + consumerTag = model.BasicConsume(queueName, true, consumer); + } + + + public void Connected(IModel newModel) + { + model = newModel; + + if (started) + Start(); + } + + + private void ClientReceived(object? sender, BasicDeliverEventArgs args) + { + MessageReceived?.Invoke(this, new MessageReceivedEventArgs( + new MessageInfo( + args.Exchange, + args.RoutingKey, + args.Body.ToArray(), + RabbitMQClientPropertiesConverter.Convert(args.BasicProperties), + args.BasicProperties.Timestamp.UnixTime > 0 + ? DateTimeOffset.FromUnixTimeSeconds(args.BasicProperties.Timestamp.UnixTime).LocalDateTime + : DateTime.Now + ) + )); + } + + } +} diff --git a/Model/RabbitMQProperties.cs b/PettingZoo.RabbitMQ/RabbitMQProperties.cs similarity index 90% rename from Model/RabbitMQProperties.cs rename to PettingZoo.RabbitMQ/RabbitMQProperties.cs index a8378b5..5cab9d6 100644 --- a/Model/RabbitMQProperties.cs +++ b/PettingZoo.RabbitMQ/RabbitMQProperties.cs @@ -1,6 +1,6 @@ -namespace PettingZoo.Model +namespace PettingZoo.RabbitMQ { - static class RabbitMQProperties + public static class RabbitMQProperties { public const string ContentType = "content-type"; public const string ContentEncoding = "content-encoding"; diff --git a/PettingZoo.RabbitMQ/RabbitMQPropertiesExtensions.cs b/PettingZoo.RabbitMQ/RabbitMQPropertiesExtensions.cs new file mode 100644 index 0000000..41cd78f --- /dev/null +++ b/PettingZoo.RabbitMQ/RabbitMQPropertiesExtensions.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace PettingZoo.RabbitMQ +{ + public static class RabbitMQPropertiesExtensions + { + public static string ContentType(this IDictionary properties) + { + return properties.TryGetValue(RabbitMQProperties.ContentType, out var value) + ? value + : ""; + } + } +} diff --git a/PettingZoo.csproj b/PettingZoo.csproj deleted file mode 100644 index 6b2b336..0000000 --- a/PettingZoo.csproj +++ /dev/null @@ -1,188 +0,0 @@ - - - - - Debug - AnyCPU - {24819D09-C747-4356-B686-D9DE9CAA6F59} - WinExe - Properties - PettingZoo - PettingZoo - v4.5 - 512 - {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - 4 - publish\ - true - Disk - false - Foreground - 7 - Days - false - false - true - 0 - 1.0.0.%2a - false - false - true - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - false - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - PettingZoo.ico - - - - packages\AutoMapper.4.2.1\lib\net45\AutoMapper.dll - True - - - packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll - True - - - packages\RabbitMQ.Client.3.6.2\lib\net45\RabbitMQ.Client.dll - True - - - packages\SimpleInjector.3.1.5\lib\net45\SimpleInjector.dll - True - - - - - - - - - - - 4.0 - - - - - - - - MSBuild:Compile - Designer - - - Designer - MSBuild:Compile - - - Designer - MSBuild:Compile - - - Designer - MSBuild:Compile - - - MSBuild:Compile - Designer - - - App.xaml - Code - - - - - - - - - - - - - - - - - - - - ConnectionWindow.xaml - - - MainWindow.xaml - Code - - - - - Code - - - True - True - Resources.resx - - - PublicResXFileCodeGenerator - Resources.Designer.cs - - - - - - - - - - False - Microsoft .NET Framework 4.5 %28x86 and x64%29 - true - - - False - .NET Framework 3.5 SP1 Client Profile - false - - - False - .NET Framework 3.5 SP1 - false - - - - - - - - \ No newline at end of file diff --git a/PettingZoo.sln b/PettingZoo.sln index 125cd4c..642c3a1 100644 --- a/PettingZoo.sln +++ b/PettingZoo.sln @@ -1,9 +1,18 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.40629.0 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31911.196 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo", "PettingZoo.csproj", "{24819D09-C747-4356-B686-D9DE9CAA6F59}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo", "PettingZoo\PettingZoo.csproj", "{24819D09-C747-4356-B686-D9DE9CAA6F59}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A64E3FB8-7606-4A05-BF10-D83FD0E80D2D}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.Core", "PettingZoo.Core\PettingZoo.Core.csproj", "{AD20CA14-6272-4C50-819D-F9FE6A963DB1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.RabbitMQ", "PettingZoo.RabbitMQ\PettingZoo.RabbitMQ.csproj", "{220149F3-A8D6-44ED-B3B6-DFE506EB018A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,8 +24,19 @@ Global {24819D09-C747-4356-B686-D9DE9CAA6F59}.Debug|Any CPU.Build.0 = Debug|Any CPU {24819D09-C747-4356-B686-D9DE9CAA6F59}.Release|Any CPU.ActiveCfg = Release|Any CPU {24819D09-C747-4356-B686-D9DE9CAA6F59}.Release|Any CPU.Build.0 = Release|Any CPU + {AD20CA14-6272-4C50-819D-F9FE6A963DB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD20CA14-6272-4C50-819D-F9FE6A963DB1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD20CA14-6272-4C50-819D-F9FE6A963DB1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD20CA14-6272-4C50-819D-F9FE6A963DB1}.Release|Any CPU.Build.0 = Release|Any CPU + {220149F3-A8D6-44ED-B3B6-DFE506EB018A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {220149F3-A8D6-44ED-B3B6-DFE506EB018A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {220149F3-A8D6-44ED-B3B6-DFE506EB018A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {220149F3-A8D6-44ED-B3B6-DFE506EB018A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {07CE270E-9E57-49CD-8D5C-79C7B7A98517} + EndGlobalSection EndGlobal diff --git a/App.xaml b/PettingZoo/App.xaml similarity index 79% rename from App.xaml rename to PettingZoo/App.xaml index a9025ab..8fa298e 100644 --- a/App.xaml +++ b/PettingZoo/App.xaml @@ -1,12 +1,12 @@  + ShutdownMode="OnMainWindowClose" + DispatcherUnhandledException="App_OnDispatcherUnhandledException"> - diff --git a/PettingZoo/App.xaml.cs b/PettingZoo/App.xaml.cs new file mode 100644 index 0000000..0a82edf --- /dev/null +++ b/PettingZoo/App.xaml.cs @@ -0,0 +1,13 @@ +using System.Windows; +using System.Windows.Threading; + +namespace PettingZoo +{ + public partial class App + { + private void App_OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) + { + _ = MessageBox.Show($"Unhandled exception: {e.Exception.Message}", "Petting Zoo - Exception", MessageBoxButton.OK, MessageBoxImage.Error); + } + } +} diff --git a/Images/Clear.svg b/PettingZoo/Images/Clear.svg similarity index 100% rename from Images/Clear.svg rename to PettingZoo/Images/Clear.svg diff --git a/Images/Connect.svg b/PettingZoo/Images/Connect.svg similarity index 100% rename from Images/Connect.svg rename to PettingZoo/Images/Connect.svg diff --git a/Images/Disconnect.svg b/PettingZoo/Images/Disconnect.svg similarity index 100% rename from Images/Disconnect.svg rename to PettingZoo/Images/Disconnect.svg diff --git a/Images/PettingZoo-48.png b/PettingZoo/Images/PettingZoo-48.png similarity index 100% rename from Images/PettingZoo-48.png rename to PettingZoo/Images/PettingZoo-48.png diff --git a/Images/PettingZoo.ai b/PettingZoo/Images/PettingZoo.ai similarity index 100% rename from Images/PettingZoo.ai rename to PettingZoo/Images/PettingZoo.ai diff --git a/Images/PettingZoo.ico b/PettingZoo/Images/PettingZoo.ico similarity index 100% rename from Images/PettingZoo.ico rename to PettingZoo/Images/PettingZoo.ico diff --git a/PettingZoo/Images/Publish.svg b/PettingZoo/Images/Publish.svg new file mode 100644 index 0000000..f83e762 --- /dev/null +++ b/PettingZoo/Images/Publish.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PettingZoo/Images/PublishSend.svg b/PettingZoo/Images/PublishSend.svg new file mode 100644 index 0000000..a564250 --- /dev/null +++ b/PettingZoo/Images/PublishSend.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PettingZoo/Images/Subscribe.svg b/PettingZoo/Images/Subscribe.svg new file mode 100644 index 0000000..72ad0cb --- /dev/null +++ b/PettingZoo/Images/Subscribe.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PettingZoo/PettingZoo.csproj b/PettingZoo/PettingZoo.csproj new file mode 100644 index 0000000..20c88b9 --- /dev/null +++ b/PettingZoo/PettingZoo.csproj @@ -0,0 +1,112 @@ + + + + WinExe + net5.0-windows + true + Mark van Renswoude + Petting Zoo + Petting Zoo - a live RabbitMQ message viewer + + enable + true + PettingZoo.Program + + + PettingZoo.ico + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ConnectionWindowStrings.resx + True + True + + + MainWindowStrings.resx + True + True + + + SubscribeWindowStrings.resx + True + True + + + PublisherViewStrings.resx + True + True + + + SubscriberViewStrings.resx + True + True + + + + + + ConnectionWindowStrings.Designer.cs + PublicResXFileCodeGenerator + + + MainWindowStrings.Designer.cs + PublicResXFileCodeGenerator + + + SubscribeWindowStrings.Designer.cs + PublicResXFileCodeGenerator + + + PublisherViewStrings.Designer.cs + PublicResXFileCodeGenerator + + + SubscriberViewStrings.Designer.cs + PublicResXFileCodeGenerator + + + + + + $(DefaultXamlRuntime) + + + $(DefaultXamlRuntime) + + + $(DefaultXamlRuntime) + + + $(DefaultXamlRuntime) + + + $(DefaultXamlRuntime) + + + + diff --git a/PettingZoo.ico b/PettingZoo/PettingZoo.ico similarity index 100% rename from PettingZoo.ico rename to PettingZoo/PettingZoo.ico diff --git a/App.xaml.cs b/PettingZoo/Program.cs similarity index 51% rename from App.xaml.cs rename to PettingZoo/Program.cs index 4089151..60752d2 100644 --- a/App.xaml.cs +++ b/PettingZoo/Program.cs @@ -5,16 +5,22 @@ using System.Reflection; using System.Windows; using System.Windows.Markup; using Newtonsoft.Json; -using PettingZoo.Model; -using PettingZoo.View; -using PettingZoo.ViewModel; +using PettingZoo.Core.Connection; +using PettingZoo.RabbitMQ; +using PettingZoo.Settings; +using PettingZoo.UI.Connection; +using PettingZoo.UI.Main; +using PettingZoo.UI.Subscribe; +using PettingZoo.UI.Tab; +using PettingZoo.UI.Tab.Subscriber; using SimpleInjector; namespace PettingZoo { - public partial class App + public static class Program { - public void ApplicationStartup(object sender, StartupEventArgs e) + [STAThread] + public static void Main() { // WPF defaults to US for date formatting in bindings, this fixes it FrameworkElement.LanguageProperty.OverrideMetadata(typeof(FrameworkElement), new FrameworkPropertyMetadata( @@ -28,28 +34,50 @@ namespace PettingZoo private static Container Bootstrap() { var container = new Container(); + + // See comments in RunApplication + container.Options.EnableAutoVerification = false; container.RegisterSingleton(() => new UserSettings(new AppDataSettingsSerializer("Settings.json"))); container.Register(); - container.Register(); + container.Register(); + container.Register(); + container.Register(); container.Register(); - container.Register(); - - // Note: don't run Verify! It'll create a MainWindow which will then become - // Application.Current.MainWindow and prevent the process from shutting down. - + return container; } - private static void RunApplication(Container container) + private static void RunApplication(Container container) { - var mainWindow = container.GetInstance(); - mainWindow.Closed += (sender, args) => container.Dispose(); + try + { + var app = new App(); + app.InitializeComponent(); - mainWindow.Show(); + #if DEBUG + // Verify container after initialization to prevent issues loading the resource dictionaries + container.Verify(); + + // This causes the MainWindow and Windows properties to be populated however, which we don't want + // because then the app does not close properly when using OnMainWindowClose, so clean up the mess + app.MainWindow = null; + foreach (var window in app.Windows) + ((Window)window).Close(); + + // All this is the reason we only perform verification in debug builds + #endif + + var mainWindow = container.GetInstance(); + _ = app.Run(mainWindow); + } + catch (Exception) + { + // TODO Log the exception and exit + } } @@ -64,8 +92,8 @@ namespace PettingZoo var companyName = GetProductInfo().Company; var productName = GetProductInfo().Product; - path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - companyName, productName); + path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + companyName, productName); fullPath = Path.Combine(path, filename); } @@ -79,7 +107,7 @@ namespace PettingZoo public void Write(UserSettings settings) { - Directory.CreateDirectory(path); + _ = Directory.CreateDirectory(path); File.WriteAllText(fullPath, JsonConvert.SerializeObject(settings, Formatting.Indented)); } @@ -87,11 +115,10 @@ namespace PettingZoo private T GetProductInfo() { var attributes = GetType().Assembly.GetCustomAttributes(typeof(T), true); - if (attributes.Length == 0) - throw new Exception("Missing product information in assembly"); - - return (T)attributes[0]; + return attributes.Length == 0 + ? throw new Exception("Missing product information in assembly") + : (T) attributes[0]; } } } -} +} \ No newline at end of file diff --git a/Model/UserSettings.cs b/PettingZoo/Settings/UserSettings.cs similarity index 91% rename from Model/UserSettings.cs rename to PettingZoo/Settings/UserSettings.cs index cefe00b..0d48a40 100644 --- a/Model/UserSettings.cs +++ b/PettingZoo/Settings/UserSettings.cs @@ -1,4 +1,4 @@ -namespace PettingZoo.Model +namespace PettingZoo.Settings { public interface IUserSettingsSerializer { @@ -15,6 +15,7 @@ public string LastUsername { get; set; } public string LastPassword { get; set; } + //public bool LastSubscribe { get; set; } public string LastExchange { get; set; } public string LastRoutingKey { get; set; } @@ -27,7 +28,7 @@ LastUsername = "guest"; LastPassword = "guest"; - LastExchange = "amqp"; + LastExchange = ""; LastRoutingKey = "#"; } } @@ -35,7 +36,7 @@ public class UserSettings { - public ConnectionWindowSettings ConnectionWindow { get; private set; } + public ConnectionWindowSettings ConnectionWindow { get; } private readonly IUserSettingsSerializer serializer; diff --git a/Style.xaml b/PettingZoo/Style.xaml similarity index 89% rename from Style.xaml rename to PettingZoo/Style.xaml index f5896b0..bc8459b 100644 --- a/Style.xaml +++ b/PettingZoo/Style.xaml @@ -1,6 +1,6 @@  + xmlns:ui="clr-namespace:PettingZoo.UI"> - @@ -59,6 +59,8 @@ - \ No newline at end of file diff --git a/PettingZoo/UI/BaseViewModel.cs b/PettingZoo/UI/BaseViewModel.cs new file mode 100644 index 0000000..33b7909 --- /dev/null +++ b/PettingZoo/UI/BaseViewModel.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace PettingZoo.UI +{ + public class BaseViewModel : INotifyPropertyChanged + { + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void RaisePropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + + protected virtual void RaiseOtherPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + + protected bool SetField(ref T field, T value, IEqualityComparer? comparer = null, [CallerMemberName] string? propertyName = null, + params string[]? otherPropertiesChanged) + { + if ((comparer ?? EqualityComparer.Default).Equals(field, value)) + return false; + + field = value; + RaisePropertyChanged(propertyName); + + if (otherPropertiesChanged == null) + return true; + + foreach (var otherProperty in otherPropertiesChanged) + RaisePropertyChanged(otherProperty); + + return true; + } + } +} \ No newline at end of file diff --git a/PettingZoo/UI/Connection/ConnectionViewModel.cs b/PettingZoo/UI/Connection/ConnectionViewModel.cs new file mode 100644 index 0000000..b1eacb6 --- /dev/null +++ b/PettingZoo/UI/Connection/ConnectionViewModel.cs @@ -0,0 +1,117 @@ +using System; +using System.Windows.Input; + +// TODO validate input + +namespace PettingZoo.UI.Connection +{ + public class ConnectionViewModel : BaseViewModel + { + private string host; + private string virtualHost; + private int port; + private string username; + private string password; + + private bool subscribe; + private string exchange; + private string routingKey; + + + public string Host + { + get => host; + set => SetField(ref host, value); + } + + public string VirtualHost + { + get => virtualHost; + set => SetField(ref virtualHost, value); + } + + public int Port + { + get => port; + set => SetField(ref port, value); + } + + public string Username + { + get => username; + set => SetField(ref username, value); + } + + public string Password + { + get => password; + set => SetField(ref password, value); + } + + + public bool Subscribe + { + get => subscribe; + set => SetField(ref subscribe, value); + } + + public string Exchange + { + get => exchange; + set => SetField(ref exchange, value); + } + + public string RoutingKey + { + get => routingKey; + set => SetField(ref routingKey, value); + } + + + public ICommand OkCommand { get; } + + public event EventHandler? OkClick; + + + public ConnectionViewModel(ConnectionDialogParams model) + { + OkCommand = new DelegateCommand(OkExecute, OkCanExecute); + + host = model.Host; + virtualHost = model.VirtualHost; + port = model.Port; + username = model.Username; + password = model.Password; + + subscribe = model.Subscribe; + exchange = model.Exchange; + routingKey = model.RoutingKey; + } + + + public ConnectionDialogParams ToModel() + { + return new(Host, VirtualHost, Port, Username, Password, Subscribe, Exchange, RoutingKey); + } + + + private void OkExecute() + { + OkClick?.Invoke(this, EventArgs.Empty); + } + + + private static bool OkCanExecute() + { + return true; + } + } + + + public class DesignTimeConnectionViewModel : ConnectionViewModel + { + public DesignTimeConnectionViewModel() : base(ConnectionDialogParams.Default) + { + } + } +} diff --git a/View/ConnectionWindow.xaml b/PettingZoo/UI/Connection/ConnectionWindow.xaml similarity index 57% rename from View/ConnectionWindow.xaml rename to PettingZoo/UI/Connection/ConnectionWindow.xaml index 700f928..0c5afd8 100644 --- a/View/ConnectionWindow.xaml +++ b/PettingZoo/UI/Connection/ConnectionWindow.xaml @@ -1,27 +1,26 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PettingZoo/UI/Main/MainWindow.xaml.cs b/PettingZoo/UI/Main/MainWindow.xaml.cs new file mode 100644 index 0000000..c41d85e --- /dev/null +++ b/PettingZoo/UI/Main/MainWindow.xaml.cs @@ -0,0 +1,50 @@ +using System; +using System.Windows; +using PettingZoo.Core.Connection; +using PettingZoo.UI.Connection; +using PettingZoo.UI.Subscribe; +using PettingZoo.UI.Tab; + +namespace PettingZoo.UI.Main +{ + // TODO support undocking tabs (and redocking afterwards) + // TODO allow tab reordering + + #pragma warning disable CA1001 // MainWindow can't be IDisposable, handled instead in OnDispatcherShutDownStarted + public partial class MainWindow + { + private readonly MainWindowViewModel viewModel; + + + public MainWindow(IConnectionFactory connectionFactory, IConnectionDialog connectionDialog, ISubscribeDialog subscribeDialog, ITabFactory tabFactory) + { + WindowStartupLocation = WindowStartupLocation.CenterScreen; + + InitializeComponent(); + viewModel = new MainWindowViewModel(connectionFactory, connectionDialog, subscribeDialog, tabFactory); + DataContext = viewModel; + + Dispatcher.ShutdownStarted += OnDispatcherShutDownStarted; + } + + + private async void OnDispatcherShutDownStarted(object? sender, EventArgs e) + { + if (DataContext is IAsyncDisposable disposable) + await disposable.DisposeAsync(); + } + + + private void MainWindow_OnLoaded(object sender, RoutedEventArgs e) + { + viewModel.ConnectCommand.Execute(null); + } + + + private void MainWindow_OnClosed(object? sender, EventArgs e) + { + var _ = Application.Current.Windows; + } + } + #pragma warning restore CA1001 +} diff --git a/Properties/Resources.Designer.cs b/PettingZoo/UI/Main/MainWindowStrings.Designer.cs similarity index 55% rename from Properties/Resources.Designer.cs rename to PettingZoo/UI/Main/MainWindowStrings.Designer.cs index 4b0bc29..8b3f52f 100644 --- a/Properties/Resources.Designer.cs +++ b/PettingZoo/UI/Main/MainWindowStrings.Designer.cs @@ -8,7 +8,10 @@ // //------------------------------------------------------------------------------ -namespace PettingZoo.Properties { +namespace PettingZoo.UI.Main { + using System; + + /// /// A strongly-typed resource class, for looking up localized strings, etc. /// @@ -16,17 +19,17 @@ namespace PettingZoo.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - public class Resources { + public class MainWindowStrings { private static global::System.Resources.ResourceManager resourceMan; private static global::System.Globalization.CultureInfo resourceCulture; [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { + internal MainWindowStrings() { } /// @@ -36,7 +39,7 @@ namespace PettingZoo.Properties { public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.Properties.Resources", typeof(Resources).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.UI.Main.MainWindowStrings", typeof(MainWindowStrings).Assembly); resourceMan = temp; } return resourceMan; @@ -58,92 +61,47 @@ namespace PettingZoo.Properties { } /// - /// Looks up a localized string similar to Cancel. + /// Looks up a localized string similar to Connect. /// - public static string ButtonCancel { + public static string CommandConnect { get { - return ResourceManager.GetString("ButtonCancel", resourceCulture); + return ResourceManager.GetString("CommandConnect", resourceCulture); } } /// - /// Looks up a localized string similar to OK. + /// Looks up a localized string similar to Disconnect. /// - public static string ButtonOK { + public static string CommandDisconnect { get { - return ResourceManager.GetString("ButtonOK", resourceCulture); + return ResourceManager.GetString("CommandDisconnect", resourceCulture); } } /// - /// Looks up a localized string similar to Exchange:. + /// Looks up a localized string similar to New Publisher. /// - public static string ConnectionExchange { + public static string CommandPublish { get { - return ResourceManager.GetString("ConnectionExchange", resourceCulture); + return ResourceManager.GetString("CommandPublish", resourceCulture); } } /// - /// Looks up a localized string similar to Host:. + /// Looks up a localized string similar to New Subscriber.... /// - public static string ConnectionHost { + public static string CommandSubscribe { get { - return ResourceManager.GetString("ConnectionHost", resourceCulture); + return ResourceManager.GetString("CommandSubscribe", resourceCulture); } } /// - /// Looks up a localized string similar to Password:. + /// Looks up a localized string similar to Close tab. /// - public static string ConnectionPassword { + public static string ContextMenuCloseTab { get { - return ResourceManager.GetString("ConnectionPassword", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Port:. - /// - public static string ConnectionPort { - get { - return ResourceManager.GetString("ConnectionPort", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Routing key:. - /// - public static string ConnectionRoutingKey { - get { - return ResourceManager.GetString("ConnectionRoutingKey", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Username:. - /// - public static string ConnectionUsername { - get { - return ResourceManager.GetString("ConnectionUsername", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Virtual host:. - /// - public static string ConnectionVirtualHost { - get { - return ResourceManager.GetString("ConnectionVirtualHost", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Connection parameters. - /// - public static string ConnectionWindowTitle { - get { - return ResourceManager.GetString("ConnectionWindowTitle", resourceCulture); + return ResourceManager.GetString("ContextMenuCloseTab", resourceCulture); } } @@ -165,51 +123,6 @@ namespace PettingZoo.Properties { } } - /// - /// Looks up a localized string similar to Petting Zoo - a RabbitMQ live message viewer. - /// - public static string MainWindowTitle { - get { - return ResourceManager.GetString("MainWindowTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Body. - /// - public static string PanelTitleBody { - get { - return ResourceManager.GetString("PanelTitleBody", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Properties. - /// - public static string PanelTitleProperties { - get { - return ResourceManager.GetString("PanelTitleProperties", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Name. - /// - public static string PropertyName { - get { - return ResourceManager.GetString("PropertyName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value. - /// - public static string PropertyValue { - get { - return ResourceManager.GetString("PropertyValue", resourceCulture); - } - } - /// /// Looks up a localized string similar to Connected. /// @@ -245,5 +158,14 @@ namespace PettingZoo.Properties { return ResourceManager.GetString("StatusError", resourceCulture); } } + + /// + /// Looks up a localized string similar to Petting Zoo - a RabbitMQ live message viewer. + /// + public static string WindowTitle { + get { + return ResourceManager.GetString("WindowTitle", resourceCulture); + } + } } } diff --git a/Properties/Resources.resx b/PettingZoo/UI/Main/MainWindowStrings.resx similarity index 83% rename from Properties/Resources.resx rename to PettingZoo/UI/Main/MainWindowStrings.resx index e9c49e4..6df2e25 100644 --- a/Properties/Resources.resx +++ b/PettingZoo/UI/Main/MainWindowStrings.resx @@ -117,35 +117,20 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Cancel + + Connect - - OK + + Disconnect - - Exchange: + + New Publisher - - Host: + + New Subscriber... - - Password: - - - Port: - - - Routing key: - - - Username: - - - Virtual host: - - - Connection parameters + + Close tab Non-persistent @@ -153,21 +138,6 @@ Persistent - - Petting Zoo - a RabbitMQ live message viewer - - - Body - - - Properties - - - Name - - - Value - Connected @@ -180,4 +150,7 @@ Error: {0} + + Petting Zoo - a RabbitMQ live message viewer + \ No newline at end of file diff --git a/PettingZoo/UI/Main/MainWindowViewModel.cs b/PettingZoo/UI/Main/MainWindowViewModel.cs new file mode 100644 index 0000000..a1b594e --- /dev/null +++ b/PettingZoo/UI/Main/MainWindowViewModel.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; +using PettingZoo.Core.Connection; +using PettingZoo.UI.Connection; +using PettingZoo.UI.Subscribe; +using PettingZoo.UI.Tab; + +namespace PettingZoo.UI.Main +{ + public class MainWindowViewModel : BaseViewModel, IAsyncDisposable + { + private readonly IConnectionFactory connectionFactory; + private readonly IConnectionDialog connectionDialog; + private readonly ISubscribeDialog subscribeDialog; + private readonly ITabFactory tabFactory; + + private ConnectionDialogParams? connectionDialogParams; + private SubscribeDialogParams? subscribeDialogParams; + private IConnection? connection; + private string connectionStatus; + private ITab? activeTab; + + private readonly DelegateCommand connectCommand; + private readonly DelegateCommand disconnectCommand; + private readonly DelegateCommand publishCommand; + private readonly DelegateCommand subscribeCommand; + private readonly DelegateCommand closeTabCommand; + + + public string ConnectionStatus + { + get => connectionStatus; + private set => SetField(ref connectionStatus, value); + } + + + public ObservableCollection Tabs { get; } + + public ITab? ActiveTab + { + get => activeTab; + set => SetField(ref activeTab, value, otherPropertiesChanged: new [] + { + nameof(ToolbarCommands), + nameof(ToolbarCommandsSeparatorVisibility) + }); + } + + public ICommand ConnectCommand => connectCommand; + public ICommand DisconnectCommand => disconnectCommand; + public ICommand PublishCommand => publishCommand; + public ICommand SubscribeCommand => subscribeCommand; + public ICommand CloseTabCommand => closeTabCommand; + + public IEnumerable ToolbarCommands => ActiveTab is ITabToolbarCommands tabToolbarCommands + ? tabToolbarCommands.ToolbarCommands + : Enumerable.Empty(); + + public Visibility ToolbarCommandsSeparatorVisibility => + ToolbarCommands.Any() ? Visibility.Visible : Visibility.Collapsed; + + + public MainWindowViewModel(IConnectionFactory connectionFactory, IConnectionDialog connectionDialog, + ISubscribeDialog subscribeDialog, ITabFactory tabFactory) + { + this.connectionFactory = connectionFactory; + this.connectionDialog = connectionDialog; + this.subscribeDialog = subscribeDialog; + this.tabFactory = tabFactory; + + connectionStatus = GetConnectionStatus(null); + + Tabs = new ObservableCollection(); + connectCommand = new DelegateCommand(ConnectExecute); + disconnectCommand = new DelegateCommand(DisconnectExecute, IsConnectedCanExecute); + publishCommand = new DelegateCommand(PublishExecute, IsConnectedCanExecute); + subscribeCommand = new DelegateCommand(SubscribeExecute, IsConnectedCanExecute); + closeTabCommand = new DelegateCommand(CloseTabExecute, CloseTabCanExecute); + } + + + public async ValueTask DisposeAsync() + { + if (connection != null) + await connection.DisposeAsync(); + } + + + private async void ConnectExecute() + { + //var newParams = connectionDialog.Show(connectionDialogParams); + var newParams = new ConnectionDialogParams("localhost", "/", 5672, "guest", "guest", true, "lef", "#"); + if (newParams == null) + return; + + if (connection != null) + await connection.DisposeAsync(); + + connectionDialogParams = newParams; + connection = connectionFactory.CreateConnection(new ConnectionParams( + connectionDialogParams.Host, connectionDialogParams.VirtualHost, connectionDialogParams.Port, + connectionDialogParams.Username, connectionDialogParams.Password)); + connection.StatusChanged += ConnectionStatusChanged; + + if (connectionDialogParams.Subscribe) + { + var subscriber = connection.Subscribe(connectionDialogParams.Exchange, connectionDialogParams.RoutingKey); + AddTab(tabFactory.CreateSubscriberTab(CloseTabCommand, subscriber)); + + } + + ConnectionChanged(); + } + + + private async void DisconnectExecute() + { + Tabs.Clear(); + + if (connection != null) + { + await connection.DisposeAsync(); + connection = null; + } + + connectionDialogParams = null; + ConnectionStatus = GetConnectionStatus(null); + + ConnectionChanged(); + } + + + private void SubscribeExecute() + { + if (connection == null) + return; + + var newParams = subscribeDialog.Show(subscribeDialogParams); + if (newParams == null) + return; + + subscribeDialogParams = newParams; + + var subscriber = connection.Subscribe(subscribeDialogParams.Exchange, subscribeDialogParams.RoutingKey); + AddTab(tabFactory.CreateSubscriberTab(CloseTabCommand, subscriber)); + } + + + private void PublishExecute() + { + if (connection == null) + return; + + AddTab(tabFactory.CreatePublisherTab(CloseTabCommand, connection)); + } + + + private bool IsConnectedCanExecute() + { + return connection != null; + } + + + private void CloseTabExecute() + { + if (ActiveTab == null) + return; + + var activeTabIndex = Tabs.IndexOf(ActiveTab); + if (activeTabIndex == -1) + return; + + Tabs.RemoveAt(activeTabIndex); + + if (activeTabIndex == Tabs.Count) + activeTabIndex--; + + ActiveTab = activeTabIndex >= 0 ? Tabs[activeTabIndex] : null; + closeTabCommand.RaiseCanExecuteChanged(); + } + + + private bool CloseTabCanExecute() + { + return ActiveTab != null; + } + + + private void AddTab(ITab tab) + { + Tabs.Add(tab); + ActiveTab = tab; + + closeTabCommand.RaiseCanExecuteChanged(); + } + + + private void ConnectionChanged() + { + disconnectCommand.RaiseCanExecuteChanged(); + subscribeCommand.RaiseCanExecuteChanged(); + publishCommand.RaiseCanExecuteChanged(); + } + + private void ConnectionStatusChanged(object? sender, StatusChangedEventArgs args) + { + ConnectionStatus = GetConnectionStatus(args); + } + + + + private static string GetConnectionStatus(StatusChangedEventArgs? args) + { + return args?.Status switch + { + Core.Connection.ConnectionStatus.Connecting => string.Format(MainWindowStrings.StatusConnecting, args.Context), + Core.Connection.ConnectionStatus.Connected => string.Format(MainWindowStrings.StatusConnected, args.Context), + Core.Connection.ConnectionStatus.Error => string.Format(MainWindowStrings.StatusError, args.Context), + Core.Connection.ConnectionStatus.Disconnected => MainWindowStrings.StatusDisconnected, + _ => MainWindowStrings.StatusDisconnected + }; + } + } + + + public class DesignTimeMainWindowViewModel : MainWindowViewModel + { + public DesignTimeMainWindowViewModel() : base(null!, null!, null!, null!) + { + } + } +} \ No newline at end of file diff --git a/Infrastructure/PasswordBoxAssistant.cs b/PettingZoo/UI/PasswordBoxAssistant.cs similarity index 92% rename from Infrastructure/PasswordBoxAssistant.cs rename to PettingZoo/UI/PasswordBoxAssistant.cs index 3ca477d..b27587b 100644 --- a/Infrastructure/PasswordBoxAssistant.cs +++ b/PettingZoo/UI/PasswordBoxAssistant.cs @@ -1,7 +1,7 @@ using System.Windows; using System.Windows.Controls; -namespace PettingZoo.Infrastructure +namespace PettingZoo.UI { // Source: http://blog.functionalfun.net/2008/06/wpf-passwordbox-and-data-binding.html public static class PasswordBoxAssistant @@ -35,9 +35,7 @@ namespace PettingZoo.Infrastructure var newPassword = (string) e.NewValue; if (!GetUpdatingPassword(box)) - { box.Password = newPassword; - } box.PasswordChanged += HandlePasswordChanged; } @@ -46,26 +44,19 @@ namespace PettingZoo.Infrastructure { // when the BindPassword attached property is set on a PasswordBox, // start listening to its PasswordChanged event - - var box = dp as PasswordBox; - - if (box == null) + if (dp is not PasswordBox box) { return; } - var wasBound = (bool) (e.OldValue); - var needToBind = (bool) (e.NewValue); + var wasBound = (bool)e.OldValue; + var needToBind = (bool)e.NewValue; if (wasBound) - { box.PasswordChanged -= HandlePasswordChanged; - } if (needToBind) - { box.PasswordChanged += HandlePasswordChanged; - } } private static void HandlePasswordChanged(object sender, RoutedEventArgs e) diff --git a/PettingZoo/UI/Subscribe/ISubscribeDialog.cs b/PettingZoo/UI/Subscribe/ISubscribeDialog.cs new file mode 100644 index 0000000..19c65e2 --- /dev/null +++ b/PettingZoo/UI/Subscribe/ISubscribeDialog.cs @@ -0,0 +1,26 @@ +using System; + +namespace PettingZoo.UI.Subscribe +{ + public interface ISubscribeDialog + { + SubscribeDialogParams? Show(SubscribeDialogParams? defaultParams = null); + } + + + public class SubscribeDialogParams + { + public string Exchange { get; } + public string RoutingKey { get; } + + + public static SubscribeDialogParams Default { get; } = new("", "#"); + + + public SubscribeDialogParams(string exchange, string routingKey) + { + Exchange = exchange; + RoutingKey = routingKey; + } + } +} diff --git a/PettingZoo/UI/Subscribe/SubscribeViewModel.cs b/PettingZoo/UI/Subscribe/SubscribeViewModel.cs new file mode 100644 index 0000000..8750367 --- /dev/null +++ b/PettingZoo/UI/Subscribe/SubscribeViewModel.cs @@ -0,0 +1,66 @@ +using System; +using System.Windows.Input; + +// TODO validate input + +namespace PettingZoo.UI.Subscribe +{ + public class SubscribeViewModel : BaseViewModel + { + private string exchange; + private string routingKey; + + + public string Exchange + { + get => exchange; + set => SetField(ref exchange, value); + } + + public string RoutingKey + { + get => routingKey; + set => SetField(ref routingKey, value); + } + + + public ICommand OkCommand { get; } + + public event EventHandler? OkClick; + + + public SubscribeViewModel(SubscribeDialogParams subscribeParams) + { + OkCommand = new DelegateCommand(OkExecute, OkCanExecute); + + exchange = subscribeParams.Exchange; + routingKey = subscribeParams.RoutingKey; + } + + + public SubscribeDialogParams ToModel() + { + return new(Exchange, RoutingKey); + } + + + private void OkExecute() + { + OkClick?.Invoke(this, EventArgs.Empty); + } + + + private static bool OkCanExecute() + { + return true; + } + } + + + public class DesignTimeSubscribeViewModel : SubscribeViewModel + { + public DesignTimeSubscribeViewModel() : base(SubscribeDialogParams.Default) + { + } + } +} diff --git a/PettingZoo/UI/Subscribe/SubscribeWindow.xaml b/PettingZoo/UI/Subscribe/SubscribeWindow.xaml new file mode 100644 index 0000000..7d8010a --- /dev/null +++ b/PettingZoo/UI/Subscribe/SubscribeWindow.xaml @@ -0,0 +1,41 @@ + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/View/MainWindow.xaml.cs b/View/MainWindow.xaml.cs deleted file mode 100644 index 9cce7cd..0000000 --- a/View/MainWindow.xaml.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Windows; -using PettingZoo.ViewModel; - -namespace PettingZoo.View -{ - public partial class MainWindow - { - public MainWindow(MainViewModel viewModel) - { - WindowStartupLocation = WindowStartupLocation.CenterScreen; - - InitializeComponent(); - DataContext = viewModel; - - Dispatcher.ShutdownStarted += OnDispatcherShutDownStarted; - } - - - private void OnDispatcherShutDownStarted(object sender, EventArgs e) - { - var disposable = DataContext as IDisposable; - if (!ReferenceEquals(null, disposable)) - disposable.Dispose(); - } - } -} diff --git a/ViewModel/ConnectionViewModel.cs b/ViewModel/ConnectionViewModel.cs deleted file mode 100644 index 367eb50..0000000 --- a/ViewModel/ConnectionViewModel.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Windows.Input; -using AutoMapper; -using PettingZoo.Infrastructure; -using PettingZoo.Model; - -namespace PettingZoo.ViewModel -{ - public class ConnectionViewModel : BaseViewModel - { - private static readonly IMapper ModelMapper = new MapperConfiguration(cfg => - cfg.CreateMap().ReverseMap() - ).CreateMapper(); - - - private readonly DelegateCommand okCommand; - - - public string Host { get; set; } - public string VirtualHost { get; set; } - public int Port { get; set; } - public string Username { get; set; } - public string Password { get; set; } - - public string Exchange { get; set; } - public string RoutingKey { get; set; } - - - public ICommand OkCommand { get { return okCommand; } } - - public event EventHandler CloseWindow; - - - public ConnectionViewModel() - { - okCommand = new DelegateCommand(OkExecute, OkCanExecute); - } - - - public ConnectionViewModel(ConnectionInfo model) : this() - { - ModelMapper.Map(model, this); - } - - - public ConnectionInfo ToModel() - { - return ModelMapper.Map(this); - } - - - private void OkExecute() - { - if (CloseWindow != null) - CloseWindow(this, EventArgs.Empty); - } - - - private bool OkCanExecute() - { - return true; - } - } -} diff --git a/ViewModel/MainViewModel.cs b/ViewModel/MainViewModel.cs deleted file mode 100644 index e392b36..0000000 --- a/ViewModel/MainViewModel.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Threading; -using System.Threading.Tasks; -using System.Windows.Input; -using PettingZoo.Infrastructure; -using PettingZoo.Model; -using PettingZoo.Properties; - -namespace PettingZoo.ViewModel -{ - public class MainViewModel : BaseViewModel, IDisposable - { - private readonly TaskScheduler uiScheduler; - private readonly IConnectionInfoBuilder connectionInfoBuilder; - private readonly IConnectionFactory connectionFactory; - - private ConnectionInfo connectionInfo; - private IConnection connection; - private string connectionStatus; - private readonly ObservableCollection messages; - private MessageInfo selectedMessage; - - private readonly DelegateCommand connectCommand; - private readonly DelegateCommand disconnectCommand; - private readonly DelegateCommand clearCommand; - - - public ConnectionInfo ConnectionInfo { - get { return connectionInfo; } - private set - { - if (value == connectionInfo) - return; - - connectionInfo = value; - RaisePropertyChanged(); - } - } - - public string ConnectionStatus - { - get { return connectionStatus; } - private set - { - if (value == connectionStatus) - return; - - connectionStatus = value; - RaisePropertyChanged(); - } - } - - public ObservableCollection Messages { get { return messages; } } - - public MessageInfo SelectedMessage - { - get { return selectedMessage; } - set - { - if (value == selectedMessage) - return; - - selectedMessage = value; - RaisePropertyChanged(); - RaiseOtherPropertyChanged("SelectedMessageBody"); - RaiseOtherPropertyChanged("SelectedMessageProperties"); - } - } - - public string SelectedMessageBody - { - get - { - return SelectedMessage != null - ? MessageBodyRenderer.Render(SelectedMessage.Body, SelectedMessage.ContentType) - : ""; - } - } - - public Dictionary SelectedMessageProperties - { - get { return SelectedMessage != null ? SelectedMessage.Properties : null; } - } - - public ICommand ConnectCommand { get { return connectCommand; } } - public ICommand DisconnectCommand { get { return disconnectCommand; } } - public ICommand ClearCommand { get { return clearCommand; } } - - - public MainViewModel(IConnectionInfoBuilder connectionInfoBuilder, IConnectionFactory connectionFactory) - { - uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(); - - this.connectionInfoBuilder = connectionInfoBuilder; - this.connectionFactory = connectionFactory; - - connectionStatus = GetConnectionStatus(null); - messages = new ObservableCollection(); - - connectCommand = new DelegateCommand(ConnectExecute); - disconnectCommand = new DelegateCommand(DisconnectExecute, DisconnectCanExecute); - clearCommand = new DelegateCommand(ClearExecute, ClearCanExecute); - } - - - public void Dispose() - { - if (connection != null) - { - connection.Dispose(); - connection = null; - } - } - - - private void ConnectExecute() - { - var newInfo = connectionInfoBuilder.Build(); - if (newInfo == null) - return; - - if (connection != null) - connection.Dispose(); - - ConnectionInfo = newInfo; - connection = connectionFactory.CreateConnection(connectionInfo); - connection.MessageReceived += ConnectionMessageReceived; - connection.StatusChanged += ConnectionStatusChanged; - - disconnectCommand.RaiseCanExecuteChanged(); - } - - - private void DisconnectExecute() - { - if (connection != null) - { - connection.Dispose(); - connection = null; - } - - ConnectionInfo = null; - ConnectionStatus = GetConnectionStatus(null); - - disconnectCommand.RaiseCanExecuteChanged(); - } - - - private bool DisconnectCanExecute() - { - return connection != null; - } - - - private void ClearExecute() - { - messages.Clear(); - clearCommand.RaiseCanExecuteChanged(); - } - - - private bool ClearCanExecute() - { - return messages.Count > 0; - } - - - private void ConnectionStatusChanged(object sender, StatusChangedEventArgs args) - { - ConnectionStatus = GetConnectionStatus(args); - } - - - private void ConnectionMessageReceived(object sender, MessageReceivedEventArgs args) - { - RunFromUiScheduler(() => - { - messages.Add(args.MessageInfo); - clearCommand.RaiseCanExecuteChanged(); - }); - } - - - private string GetConnectionStatus(StatusChangedEventArgs args) - { - if (args != null) - switch (args.Status) - { - case Model.ConnectionStatus.Connecting: - return String.Format(Resources.StatusConnecting, args.Context); - - case Model.ConnectionStatus.Connected: - return String.Format(Resources.StatusConnected, args.Context); - - case Model.ConnectionStatus.Error: - return String.Format(Resources.StatusError, args.Context); - } - - return Resources.StatusDisconnected; - } - - - private void RunFromUiScheduler(Action action) - { - Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.None, uiScheduler); - } - } -} \ No newline at end of file diff --git a/packages.config b/packages.config deleted file mode 100644 index 7458215..0000000 --- a/packages.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file From 503507422d187df12dcbf0430e702e12961a4d5c Mon Sep 17 00:00:00 2001 From: Mark van Renswoude Date: Sun, 28 Nov 2021 17:50:17 +0100 Subject: [PATCH 02/16] Added prototype for converting message classes into example JSON --- .../AssemblyLoaderMessageParser.cs | 133 ++++++++++++++ .../MetadataReaderMessageParser.cs | 164 ++++++++++++++++++ .../ParseTapetiMessagesPrototype.csproj | 12 ++ ParseTapetiMessagesPrototype/Program.cs | 21 +++ PettingZoo.sln | 10 +- 5 files changed, 338 insertions(+), 2 deletions(-) create mode 100644 ParseTapetiMessagesPrototype/AssemblyLoaderMessageParser.cs create mode 100644 ParseTapetiMessagesPrototype/MetadataReaderMessageParser.cs create mode 100644 ParseTapetiMessagesPrototype/ParseTapetiMessagesPrototype.csproj create mode 100644 ParseTapetiMessagesPrototype/Program.cs diff --git a/ParseTapetiMessagesPrototype/AssemblyLoaderMessageParser.cs b/ParseTapetiMessagesPrototype/AssemblyLoaderMessageParser.cs new file mode 100644 index 0000000..1bfbc66 --- /dev/null +++ b/ParseTapetiMessagesPrototype/AssemblyLoaderMessageParser.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using Newtonsoft.Json.Linq; + +namespace ParseTapetiMessagesPrototype +{ + public static class AssemblyLoaderMessageParser + { + public static void ParseAssembly(string classLibraryFilename) + { + var loadContext = new AssemblyLoadContext(null, true); + try + { + var assembly = loadContext.LoadFromAssemblyPath(classLibraryFilename); + + foreach (var assemblyType in assembly.GetTypes()) + HandleType(assemblyType); + } + finally + { + loadContext.Unload(); + } + } + + + private static void HandleType(Type type) + { + if (!type.IsClass) + return; + + // For this prototype, filter out anything not ending in Message + // Might want to show a full tree in PettingZoo since this is just a convention + if (!type.Name.EndsWith("Message") || type.Name != "RelatieUpdateMessage") + return; + + Console.WriteLine($"{type.Namespace}.{type.Name}"); + + // 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 slightly 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. + var serialized = TypeToJObject(type); + + Console.WriteLine(serialized); + Console.WriteLine(""); + } + + + private static JObject TypeToJObject(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(TypeToJObject(enumerableInterface.GetGenericArguments()[0])); + + + return TypeToJObject(actualType); + } + + if (actualType.IsArray) + return new JArray(TypeToJObject(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/ParseTapetiMessagesPrototype/MetadataReaderMessageParser.cs b/ParseTapetiMessagesPrototype/MetadataReaderMessageParser.cs new file mode 100644 index 0000000..f3081cb --- /dev/null +++ b/ParseTapetiMessagesPrototype/MetadataReaderMessageParser.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Immutable; +using System.IO; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using System.Text; +using Newtonsoft.Json.Linq; + +namespace ParseTapetiMessagesPrototype +{ + public static class MetadataReaderMessageParser + { + public static void ParseAssembly(string classLibraryFilename) + { + try + { + using var fileStream = new FileStream(classLibraryFilename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using var peReader = new PEReader(fileStream); + + var metadataReader = peReader.GetMetadataReader(); + + // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator + foreach (var typeDefinitionHandle in metadataReader.TypeDefinitions) + { + var typeDefinition = metadataReader.GetTypeDefinition(typeDefinitionHandle); + HandleTypeDefinition(metadataReader, typeDefinition); + } + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + } + + + private static void HandleTypeDefinition(MetadataReader metadataReader, TypeDefinition typeDefinition) + { + var typeNamespace = metadataReader.GetString(typeDefinition.Namespace); + var typeName = metadataReader.GetString(typeDefinition.Name); + + // For this prototype, filter out anything not ending in Message + // Might want to show a full tree in PettingZoo since this is just a convention + if (!typeName.EndsWith("Message")) + return; + + Console.WriteLine($"{typeNamespace}.{typeName}"); + + var example = new JObject(); + + // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator + foreach (var propertyDefinitionHandle in typeDefinition.GetProperties()) + { + // TODO get properties from base class + + var propertyDefinition = metadataReader.GetPropertyDefinition(propertyDefinitionHandle); + HandlePropertyDefinition(metadataReader, propertyDefinition, example); + } + + Console.WriteLine(example.ToString()); + Console.WriteLine(); + } + + private static void HandlePropertyDefinition(MetadataReader metadataReader, PropertyDefinition propertyDefinition, JObject targetObject) + { + var fieldName = metadataReader.GetString(propertyDefinition.Name); + var signature = propertyDefinition.DecodeSignature(new JsonSignatureProvider(), null); + + targetObject.Add(fieldName, signature.ReturnType); + } + + + private class JsonSignatureProvider : ISignatureTypeProvider + { + public JToken GetPrimitiveType(PrimitiveTypeCode typeCode) + { + return typeCode switch + { + PrimitiveTypeCode.Boolean => false, + + PrimitiveTypeCode.Byte or + PrimitiveTypeCode.Int16 or + PrimitiveTypeCode.Int32 or + PrimitiveTypeCode.Int64 or + PrimitiveTypeCode.IntPtr or + PrimitiveTypeCode.SByte or + PrimitiveTypeCode.UInt16 or + PrimitiveTypeCode.UInt32 or + PrimitiveTypeCode.UInt64 or + PrimitiveTypeCode.UIntPtr => 0, + + PrimitiveTypeCode.Char or + PrimitiveTypeCode.String => "", + + PrimitiveTypeCode.Double or + PrimitiveTypeCode.Single => 0.0, + + // TODO recurse + PrimitiveTypeCode.Object => "OBJECT", + + _ => $"Unsupported primitive type code: {typeCode}" + }; + } + + public JToken GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind = 0) => "typedef"; + + public JToken GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind = 0) + { + var typeReference = reader.GetTypeReference(handle); + var typeName = reader.GetString(typeReference.Name); + + return typeName; + } + + + public JToken GetTypeFromSpecification(MetadataReader reader, object genericContext, TypeSpecificationHandle handle, byte rawTypeKind = 0) => "typespec"; + + public JToken GetSZArrayType(JToken elementType) => new JValue(elementType + "[]"); + public JToken GetPointerType(JToken elementType) => null; + public JToken GetByReferenceType(JToken elementType) => null; + public JToken GetGenericMethodParameter(object genericContext, int index) => "!!" + index; + public JToken GetGenericTypeParameter(object genericContext, int index) => "!" + index; + + public JToken GetPinnedType(JToken elementType) => elementType + " pinned"; + public JToken GetGenericInstantiation(JToken genericType, ImmutableArray typeArguments) => genericType + "<" + string.Join(",", typeArguments) + ">"; + public JToken GetModifiedType(JToken modifierType, JToken unmodifiedType, bool isRequired) => unmodifiedType + (isRequired ? " modreq(" : " modopt(") + modifierType + ")"; + + public JToken GetArrayType(JToken elementType, ArrayShape shape) + { + var builder = new StringBuilder(); + + builder.Append(elementType); + builder.Append('['); + + for (int i = 0; i < shape.Rank; i++) + { + int lowerBound = 0; + + if (i < shape.LowerBounds.Length) + { + lowerBound = shape.LowerBounds[i]; + builder.Append(lowerBound); + } + + builder.Append("..."); + + if (i < shape.Sizes.Length) + { + builder.Append(lowerBound + shape.Sizes[i] - 1); + } + + if (i < shape.Rank - 1) + { + builder.Append(','); + } + } + + builder.Append(']'); + return builder.ToString(); + } + + public JToken GetFunctionPointerType(MethodSignature signature) => "methodptr(something)"; + } + } +} diff --git a/ParseTapetiMessagesPrototype/ParseTapetiMessagesPrototype.csproj b/ParseTapetiMessagesPrototype/ParseTapetiMessagesPrototype.csproj new file mode 100644 index 0000000..b84e63f --- /dev/null +++ b/ParseTapetiMessagesPrototype/ParseTapetiMessagesPrototype.csproj @@ -0,0 +1,12 @@ + + + + Exe + net5.0 + + + + + + + diff --git a/ParseTapetiMessagesPrototype/Program.cs b/ParseTapetiMessagesPrototype/Program.cs new file mode 100644 index 0000000..9b753ce --- /dev/null +++ b/ParseTapetiMessagesPrototype/Program.cs @@ -0,0 +1,21 @@ +namespace ParseTapetiMessagesPrototype +{ + public class Program + { + public static void Main() + { + const string classLibraryFilename = "D:\\Temp\\lib\\netstandard2.0\\Messaging.Relatie.dll"; + + // There are advantages to using the MetadataReader, for example no code is run (LoadAssemblyForReflection is no longer + // supported in .NET Core) and the assembly is not locked at all. This comes at the cost of complexity however, so + // this prototype explores both options. + // + // In the final version perhaps we can work around loading the assembly into our own process by spawning a new process + // to convert it into metadata used by the main process. + + //MetadataReaderMessageParser.ParseAssembly(classLibraryFilename); + + AssemblyLoaderMessageParser.ParseAssembly(classLibraryFilename); + } + } +} diff --git a/PettingZoo.sln b/PettingZoo.sln index 642c3a1..f4d15a5 100644 --- a/PettingZoo.sln +++ b/PettingZoo.sln @@ -10,9 +10,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.Core", "PettingZoo.Core\PettingZoo.Core.csproj", "{AD20CA14-6272-4C50-819D-F9FE6A963DB1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.Core", "PettingZoo.Core\PettingZoo.Core.csproj", "{AD20CA14-6272-4C50-819D-F9FE6A963DB1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.RabbitMQ", "PettingZoo.RabbitMQ\PettingZoo.RabbitMQ.csproj", "{220149F3-A8D6-44ED-B3B6-DFE506EB018A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.RabbitMQ", "PettingZoo.RabbitMQ\PettingZoo.RabbitMQ.csproj", "{220149F3-A8D6-44ED-B3B6-DFE506EB018A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParseTapetiMessagesPrototype", "ParseTapetiMessagesPrototype\ParseTapetiMessagesPrototype.csproj", "{B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -32,6 +34,10 @@ Global {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 + {B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 229fc9415d419e8b0d4d0d82a21618fe9574f365 Mon Sep 17 00:00:00 2001 From: Mark van Renswoude Date: Mon, 6 Dec 2021 14:08:29 +0100 Subject: [PATCH 03/16] Implemented raw publishing --- PettingZoo.Core/Connection/IConnection.cs | 2 +- PettingZoo.Core/Connection/ISubscriber.cs | 4 +- PettingZoo.Core/Connection/MessageInfo.cs | 61 +++- PettingZoo.Core/PettingZoo.Core.csproj | 15 + .../Rendering/MessageBodyRenderer.cs | 4 +- .../Rendering/MessagePropertiesRenderer.cs | 60 ++++ ...ssagePropertiesRendererStrings.Designer.cs | 198 +++++++++++ .../MessagePropertiesRendererStrings.resx | 165 +++++++++ .../RabbitMQClientConnection.cs | 2 +- .../RabbitMQClientPropertiesConverter.cs | 213 ++++++------ .../RabbitMQClientSubscriber.cs | 2 +- PettingZoo.RabbitMQ/RabbitMQProperties.cs | 19 -- .../RabbitMQPropertiesExtensions.cs | 14 - PettingZoo/PettingZoo.csproj | 9 + PettingZoo/Program.cs | 1 - PettingZoo/Style.xaml | 14 + PettingZoo/UI/Main/MainWindow.xaml | 2 +- PettingZoo/UI/Main/MainWindow.xaml.cs | 3 +- PettingZoo/UI/Main/MainWindowViewModel.cs | 7 +- .../UI/Tab/Publisher/PublisherView.xaml | 17 +- .../UI/Tab/Publisher/PublisherViewModel.cs | 71 +++- .../UI/Tab/Publisher/RawPublisherView.xaml | 166 +++++++++ .../UI/Tab/Publisher/RawPublisherView.xaml.cs | 67 ++++ .../UI/Tab/Publisher/RawPublisherViewModel.cs | 322 ++++++++++++++++++ .../RawPublisherViewStrings.Designer.cs | 297 ++++++++++++++++ .../Publisher/RawPublisherViewStrings.resx | 198 +++++++++++ .../UI/Tab/Subscriber/SubscriberView.xaml | 4 +- .../UI/Tab/Subscriber/SubscriberViewModel.cs | 42 ++- 28 files changed, 1786 insertions(+), 193 deletions(-) create mode 100644 PettingZoo.Core/Rendering/MessagePropertiesRenderer.cs create mode 100644 PettingZoo.Core/Rendering/MessagePropertiesRendererStrings.Designer.cs create mode 100644 PettingZoo.Core/Rendering/MessagePropertiesRendererStrings.resx delete mode 100644 PettingZoo.RabbitMQ/RabbitMQProperties.cs delete mode 100644 PettingZoo.RabbitMQ/RabbitMQPropertiesExtensions.cs create mode 100644 PettingZoo/UI/Tab/Publisher/RawPublisherView.xaml create mode 100644 PettingZoo/UI/Tab/Publisher/RawPublisherView.xaml.cs create mode 100644 PettingZoo/UI/Tab/Publisher/RawPublisherViewModel.cs create mode 100644 PettingZoo/UI/Tab/Publisher/RawPublisherViewStrings.Designer.cs create mode 100644 PettingZoo/UI/Tab/Publisher/RawPublisherViewStrings.resx diff --git a/PettingZoo.Core/Connection/IConnection.cs b/PettingZoo.Core/Connection/IConnection.cs index f29014c..4f4d0da 100644 --- a/PettingZoo.Core/Connection/IConnection.cs +++ b/PettingZoo.Core/Connection/IConnection.cs @@ -8,7 +8,7 @@ namespace PettingZoo.Core.Connection event EventHandler StatusChanged; ISubscriber Subscribe(string exchange, string routingKey); - Task Publish(MessageInfo messageInfo); + Task Publish(PublishMessageInfo messageInfo); } diff --git a/PettingZoo.Core/Connection/ISubscriber.cs b/PettingZoo.Core/Connection/ISubscriber.cs index 3ec09c8..0148cc4 100644 --- a/PettingZoo.Core/Connection/ISubscriber.cs +++ b/PettingZoo.Core/Connection/ISubscriber.cs @@ -15,10 +15,10 @@ namespace PettingZoo.Core.Connection public class MessageReceivedEventArgs : EventArgs { - public MessageInfo MessageInfo { get; } + public ReceivedMessageInfo MessageInfo { get; } - public MessageReceivedEventArgs(MessageInfo messageInfo) + public MessageReceivedEventArgs(ReceivedMessageInfo messageInfo) { MessageInfo = messageInfo; } diff --git a/PettingZoo.Core/Connection/MessageInfo.cs b/PettingZoo.Core/Connection/MessageInfo.cs index f70abd6..fdc6e86 100644 --- a/PettingZoo.Core/Connection/MessageInfo.cs +++ b/PettingZoo.Core/Connection/MessageInfo.cs @@ -3,21 +3,72 @@ using System.Collections.Generic; namespace PettingZoo.Core.Connection { - public class MessageInfo + public class BaseMessageInfo { - public DateTime Timestamp { get; } public string Exchange { get; } public string RoutingKey { get; } public byte[] Body { get; } - public IDictionary Properties { get; } + public MessageProperties Properties { get; } - public MessageInfo(string exchange, string routingKey, byte[] body, IDictionary properties, DateTime timestamp) + public BaseMessageInfo(string exchange, string routingKey, byte[] body, MessageProperties properties) { Exchange = exchange; RoutingKey = routingKey; Body = body; Properties = properties; - Timestamp = timestamp; } } + + + 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/PettingZoo.Core.csproj b/PettingZoo.Core/PettingZoo.Core.csproj index 951e5aa..b380728 100644 --- a/PettingZoo.Core/PettingZoo.Core.csproj +++ b/PettingZoo.Core/PettingZoo.Core.csproj @@ -9,4 +9,19 @@ + + + True + True + MessagePropertiesRendererStrings.resx + + + + + + ResXFileCodeGenerator + MessagePropertiesRendererStrings.Designer.cs + + + diff --git a/PettingZoo.Core/Rendering/MessageBodyRenderer.cs b/PettingZoo.Core/Rendering/MessageBodyRenderer.cs index acdc033..f3d4128 100644 --- a/PettingZoo.Core/Rendering/MessageBodyRenderer.cs +++ b/PettingZoo.Core/Rendering/MessageBodyRenderer.cs @@ -13,9 +13,9 @@ namespace PettingZoo.Core.Rendering }; - public static string Render(byte[] body, string contentType = "") + public static string Render(byte[] body, string? contentType) { - return ContentTypeHandlers.TryGetValue(contentType, out var handler) + return (contentType != null) && ContentTypeHandlers.TryGetValue(contentType, out var handler) ? handler(body) : 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.RabbitMQ/RabbitMQClientConnection.cs b/PettingZoo.RabbitMQ/RabbitMQClientConnection.cs index 9682070..a6f54e5 100644 --- a/PettingZoo.RabbitMQ/RabbitMQClientConnection.cs +++ b/PettingZoo.RabbitMQ/RabbitMQClientConnection.cs @@ -81,7 +81,7 @@ namespace PettingZoo.RabbitMQ } - public Task Publish(MessageInfo messageInfo) + public Task Publish(PublishMessageInfo messageInfo) { if (model == null) throw new InvalidOperationException("Not connected"); diff --git a/PettingZoo.RabbitMQ/RabbitMQClientPropertiesConverter.cs b/PettingZoo.RabbitMQ/RabbitMQClientPropertiesConverter.cs index 967413f..8ad2be5 100644 --- a/PettingZoo.RabbitMQ/RabbitMQClientPropertiesConverter.cs +++ b/PettingZoo.RabbitMQ/RabbitMQClientPropertiesConverter.cs @@ -1,136 +1,135 @@ -using System.Collections.Generic; -using System.Globalization; +using System; +using System.Linq; using System.Text; +using PettingZoo.Core.Connection; using RabbitMQ.Client; namespace PettingZoo.RabbitMQ { public static class RabbitMQClientPropertiesConverter { - public static IDictionary Convert(IBasicProperties basicProperties) + public static MessageProperties Convert(IBasicProperties basicProperties) { - var properties = new Dictionary(); - - if (basicProperties.IsDeliveryModePresent()) - properties.Add(RabbitMQProperties.DeliveryMode, basicProperties.DeliveryMode.ToString(CultureInfo.InvariantCulture)); - - if (basicProperties.IsContentTypePresent()) - properties.Add(RabbitMQProperties.ContentType, basicProperties.ContentType); - - if (basicProperties.IsContentEncodingPresent()) - properties.Add(RabbitMQProperties.ContentEncoding, basicProperties.ContentEncoding); - - if (basicProperties.IsPriorityPresent()) - properties.Add(RabbitMQProperties.Priority, basicProperties.Priority.ToString(CultureInfo.InvariantCulture)); - - if (basicProperties.IsCorrelationIdPresent()) - properties.Add(RabbitMQProperties.Priority, basicProperties.CorrelationId); - - if (basicProperties.IsReplyToPresent()) - properties.Add(RabbitMQProperties.ReplyTo, basicProperties.ReplyTo); - - if (basicProperties.IsExpirationPresent()) - properties.Add(RabbitMQProperties.Expiration, basicProperties.Expiration); - - if (basicProperties.IsMessageIdPresent()) - properties.Add(RabbitMQProperties.MessageId, basicProperties.MessageId); - - if (basicProperties.IsTimestampPresent()) - properties.Add(RabbitMQProperties.Timestamp, basicProperties.Timestamp.UnixTime.ToString(CultureInfo.InvariantCulture)); - - if (basicProperties.IsTypePresent()) - properties.Add(RabbitMQProperties.Type, basicProperties.Type); - - if (basicProperties.IsUserIdPresent()) - properties.Add(RabbitMQProperties.UserId, basicProperties.UserId); - - if (basicProperties.IsAppIdPresent()) - properties.Add(RabbitMQProperties.UserId, basicProperties.AppId); - - if (basicProperties.IsClusterIdPresent()) - properties.Add(RabbitMQProperties.ClusterId, basicProperties.ClusterId); - - // ReSharper disable once InvertIf - if (basicProperties.Headers != null) + return new MessageProperties(basicProperties.Headers?.ToDictionary(p => p.Key, p => Encoding.UTF8.GetString((byte[])p.Value))) { - foreach (var (key, value) in basicProperties.Headers) - properties.Add(key, Encoding.UTF8.GetString((byte[]) value)); - } + DeliveryMode = basicProperties.IsDeliveryModePresent() + ? basicProperties.DeliveryMode == 2 ? MessageDeliveryMode.Persistent : + MessageDeliveryMode.NonPersistent + : null, - return properties; + 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(IDictionary properties, IBasicProperties targetProperties) + public static IBasicProperties Convert(MessageProperties properties, IBasicProperties targetProperties) { - foreach (var (key, value) in properties) - { - switch (key) - { - case RabbitMQProperties.DeliveryMode: - if (byte.TryParse(value, out var deliveryMode)) - targetProperties.DeliveryMode = deliveryMode; + if (properties.DeliveryMode != null) + targetProperties.DeliveryMode = properties.DeliveryMode == MessageDeliveryMode.Persistent ? (byte)2 : (byte)1; + else + targetProperties.ClearDeliveryMode(); - break; + if (properties.ContentType != null) + targetProperties.ContentType = properties.ContentType; + else + targetProperties.ClearContentType(); - case RabbitMQProperties.ContentType: - targetProperties.ContentType = value; - break; + if (properties.ContentEncoding != null) + targetProperties.ContentEncoding = properties.ContentEncoding; + else + targetProperties.ClearContentEncoding(); - case RabbitMQProperties.ContentEncoding: - targetProperties.ContentEncoding = value; - break; + if (properties.Priority != null) + targetProperties.Priority = properties.Priority.Value; + else + targetProperties.ClearPriority(); - case RabbitMQProperties.Priority: - if (byte.TryParse(value, out var priority)) - targetProperties.Priority = priority; - - break; + if (properties.CorrelationId != null) + targetProperties.CorrelationId = properties.CorrelationId; + else + targetProperties.ClearCorrelationId(); - case RabbitMQProperties.CorrelationId: - targetProperties.CorrelationId = value; - break; - - case RabbitMQProperties.ReplyTo: - targetProperties.ReplyTo = value; - break; + if (properties.ReplyTo != null) + targetProperties.ReplyTo = properties.ReplyTo; + else + targetProperties.ClearReplyTo(); - case RabbitMQProperties.Expiration: - targetProperties.Expiration = value; - break; + if (properties.Expiration != null) + targetProperties.Expiration = properties.Expiration; + else + targetProperties.ClearExpiration(); - case RabbitMQProperties.MessageId: - targetProperties.MessageId = value; - break; + if (properties.MessageId != null) + targetProperties.MessageId = properties.MessageId; + else + targetProperties.ClearMessageId(); - case RabbitMQProperties.Timestamp: - if (long.TryParse(value, out var timestamp)) - targetProperties.Timestamp = new AmqpTimestamp(timestamp); - - break; + if (properties.Timestamp != null) + targetProperties.Timestamp = new AmqpTimestamp(new DateTimeOffset(properties.Timestamp.Value).ToUnixTimeMilliseconds()); + else + targetProperties.ClearTimestamp(); - case RabbitMQProperties.Type: - targetProperties.Type = value; - break; + if (properties.Type != null) + targetProperties.Type = properties.Type; + else + targetProperties.ClearType(); - case RabbitMQProperties.UserId: - targetProperties.UserId = value; - break; + if (properties.UserId != null) + targetProperties.UserId = properties.UserId; + else + targetProperties.ClearUserId(); - case RabbitMQProperties.AppId: - targetProperties.AppId = value; - break; + if (properties.AppId != null) + targetProperties.AppId = properties.AppId; + else + targetProperties.ClearAppId(); - case RabbitMQProperties.ClusterId: - targetProperties.ClusterId = value; - break; - - default: - targetProperties.Headers ??= new Dictionary(); - targetProperties.Headers.Add(key, Encoding.UTF8.GetBytes(value)); - break; - } - } + 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 index 7f7dd26..bc07d37 100644 --- a/PettingZoo.RabbitMQ/RabbitMQClientSubscriber.cs +++ b/PettingZoo.RabbitMQ/RabbitMQClientSubscriber.cs @@ -63,7 +63,7 @@ namespace PettingZoo.RabbitMQ private void ClientReceived(object? sender, BasicDeliverEventArgs args) { MessageReceived?.Invoke(this, new MessageReceivedEventArgs( - new MessageInfo( + new ReceivedMessageInfo( args.Exchange, args.RoutingKey, args.Body.ToArray(), diff --git a/PettingZoo.RabbitMQ/RabbitMQProperties.cs b/PettingZoo.RabbitMQ/RabbitMQProperties.cs deleted file mode 100644 index 5cab9d6..0000000 --- a/PettingZoo.RabbitMQ/RabbitMQProperties.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace PettingZoo.RabbitMQ -{ - public 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/PettingZoo.RabbitMQ/RabbitMQPropertiesExtensions.cs b/PettingZoo.RabbitMQ/RabbitMQPropertiesExtensions.cs deleted file mode 100644 index 41cd78f..0000000 --- a/PettingZoo.RabbitMQ/RabbitMQPropertiesExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; - -namespace PettingZoo.RabbitMQ -{ - public static class RabbitMQPropertiesExtensions - { - public static string ContentType(this IDictionary properties) - { - return properties.TryGetValue(RabbitMQProperties.ContentType, out var value) - ? value - : ""; - } - } -} diff --git a/PettingZoo/PettingZoo.csproj b/PettingZoo/PettingZoo.csproj index 20c88b9..cd97af0 100644 --- a/PettingZoo/PettingZoo.csproj +++ b/PettingZoo/PettingZoo.csproj @@ -56,6 +56,11 @@ True True + + RawPublisherViewStrings.resx + True + True + PublisherViewStrings.resx True @@ -81,6 +86,10 @@ SubscribeWindowStrings.Designer.cs PublicResXFileCodeGenerator + + RawPublisherViewStrings.Designer.cs + PublicResXFileCodeGenerator + PublisherViewStrings.Designer.cs PublicResXFileCodeGenerator diff --git a/PettingZoo/Program.cs b/PettingZoo/Program.cs index 60752d2..2940ce7 100644 --- a/PettingZoo/Program.cs +++ b/PettingZoo/Program.cs @@ -12,7 +12,6 @@ using PettingZoo.UI.Connection; using PettingZoo.UI.Main; using PettingZoo.UI.Subscribe; using PettingZoo.UI.Tab; -using PettingZoo.UI.Tab.Subscriber; using SimpleInjector; namespace PettingZoo diff --git a/PettingZoo/Style.xaml b/PettingZoo/Style.xaml index bc8459b..5f5e226 100644 --- a/PettingZoo/Style.xaml +++ b/PettingZoo/Style.xaml @@ -10,6 +10,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/PettingZoo/UI/Main/MainWindow.xaml b/PettingZoo/UI/Main/MainWindow.xaml index 566aa8d..6194a98 100644 --- a/PettingZoo/UI/Main/MainWindow.xaml +++ b/PettingZoo/UI/Main/MainWindow.xaml @@ -9,7 +9,7 @@ mc:Ignorable="d" d:DataContext="{d:DesignInstance main:DesignTimeMainWindowViewModel, IsDesignTimeCreatable=True}" Width="800" - Height="600" + Height="800" ResizeMode="CanResizeWithGrip" Style="{StaticResource WindowStyle}" Title="{x:Static main:MainWindowStrings.WindowTitle}" diff --git a/PettingZoo/UI/Main/MainWindow.xaml.cs b/PettingZoo/UI/Main/MainWindow.xaml.cs index c41d85e..2f033ff 100644 --- a/PettingZoo/UI/Main/MainWindow.xaml.cs +++ b/PettingZoo/UI/Main/MainWindow.xaml.cs @@ -37,7 +37,8 @@ namespace PettingZoo.UI.Main private void MainWindow_OnLoaded(object sender, RoutedEventArgs e) { - viewModel.ConnectCommand.Execute(null); + // TODO support command-line parameters for easier testing + //viewModel.ConnectCommand.Execute(null); } diff --git a/PettingZoo/UI/Main/MainWindowViewModel.cs b/PettingZoo/UI/Main/MainWindowViewModel.cs index a1b594e..ad71060 100644 --- a/PettingZoo/UI/Main/MainWindowViewModel.cs +++ b/PettingZoo/UI/Main/MainWindowViewModel.cs @@ -93,8 +93,11 @@ namespace PettingZoo.UI.Main private async void ConnectExecute() { - //var newParams = connectionDialog.Show(connectionDialogParams); - var newParams = new ConnectionDialogParams("localhost", "/", 5672, "guest", "guest", true, "lef", "#"); + var newParams = connectionDialog.Show(connectionDialogParams); + + // TODO support command-line parameters for easier testing + // var newParams = new ConnectionDialogParams("localhost", "/", 5672, "guest", "guest", true, "test", "#"); + if (newParams == null) return; diff --git a/PettingZoo/UI/Tab/Publisher/PublisherView.xaml b/PettingZoo/UI/Tab/Publisher/PublisherView.xaml index 7167b3e..accf7fb 100644 --- a/PettingZoo/UI/Tab/Publisher/PublisherView.xaml +++ b/PettingZoo/UI/Tab/Publisher/PublisherView.xaml @@ -9,13 +9,20 @@ d:DesignWidth="800" d:DataContext="{d:DesignInstance res:DesignTimePublisherViewModel, IsDesignTimeCreatable=True}" Background="White"> - - - + + + + + + + + - TODO: implement publish forms - + + + + diff --git a/PettingZoo/UI/Tab/Publisher/PublisherViewModel.cs b/PettingZoo/UI/Tab/Publisher/PublisherViewModel.cs index a3542e2..3bb6e75 100644 --- a/PettingZoo/UI/Tab/Publisher/PublisherViewModel.cs +++ b/PettingZoo/UI/Tab/Publisher/PublisherViewModel.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Windows.Controls; using System.Windows.Input; using PettingZoo.Core.Connection; @@ -16,6 +18,11 @@ namespace PettingZoo.UI.Tab.Publisher private readonly IConnection connection; private MessageType messageType; + private UserControl? messageTypeControl; + private ICommand? messageTypePublishCommand; + + private UserControl? rawPublisherView; + private UserControl? tapetiPublisherView; private readonly DelegateCommand publishCommand; private readonly TabToolbarCommand[] toolbarCommands; @@ -24,15 +31,20 @@ namespace PettingZoo.UI.Tab.Publisher public MessageType MessageType { get => messageType; - set => SetField(ref messageType, value, - otherPropertiesChanged: new[] + set + { + if (SetField(ref messageType, value, + otherPropertiesChanged: new[] + { + nameof(MessageTypeRaw), + nameof(MessageTypeTapeti) + })) { - nameof(MessageTypeRaw), - nameof(MessageTypeTapeti) - }); - + SetMessageTypeControl(value); + } + } } - + public bool MessageTypeRaw { get => MessageType == MessageType.Raw; @@ -46,6 +58,13 @@ namespace PettingZoo.UI.Tab.Publisher } + public UserControl? MessageTypeControl + { + get => messageTypeControl; + set => SetField(ref messageTypeControl, value); + } + + public ICommand PublishCommand => publishCommand; @@ -64,19 +83,49 @@ namespace PettingZoo.UI.Tab.Publisher { new TabToolbarCommand(PublishCommand, PublisherViewStrings.CommandPublish, SvgIconHelper.LoadFromResource("/Images/PublishSend.svg")) }; + + SetMessageTypeControl(MessageType.Raw); } private void PublishExecute() { - // TODO + messageTypePublishCommand?.Execute(null); } private bool PublishCanExecute() { - // TODO validate input - return true; + return messageTypePublishCommand?.CanExecute(null) ?? false; + } + + + private void SetMessageTypeControl(MessageType value) + { + switch (value) + { + case MessageType.Raw: + var rawPublisherViewModel = new RawPublisherViewModel(connection); + rawPublisherView ??= new RawPublisherView(rawPublisherViewModel); + MessageTypeControl = rawPublisherView; + + messageTypePublishCommand = rawPublisherViewModel.PublishCommand; + publishCommand.RaiseCanExecuteChanged(); + break; + + case MessageType.Tapeti: + // TODO + var tapetiPublisherViewModel = new RawPublisherViewModel(connection); + tapetiPublisherView ??= new RawPublisherView(tapetiPublisherViewModel); + MessageTypeControl = tapetiPublisherView; + + messageTypePublishCommand = tapetiPublisherViewModel.PublishCommand; + publishCommand.RaiseCanExecuteChanged(); + break; + + default: + throw new ArgumentException(); + } } } diff --git a/PettingZoo/UI/Tab/Publisher/RawPublisherView.xaml b/PettingZoo/UI/Tab/Publisher/RawPublisherView.xaml new file mode 100644 index 0000000..fa939b0 --- /dev/null +++ b/PettingZoo/UI/Tab/Publisher/RawPublisherView.xaml @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PettingZoo/UI/Tab/Publisher/RawPublisherView.xaml.cs b/PettingZoo/UI/Tab/Publisher/RawPublisherView.xaml.cs new file mode 100644 index 0000000..2b8fab1 --- /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 RawPublisherViewModel viewModel; + private DispatcherTimer checkEmptyHeaderTimer; + + + public RawPublisherView(RawPublisherViewModel viewModel) + { + this.viewModel = viewModel; + + InitializeComponent(); + DataContext = viewModel; + + 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..d5d8cd3 --- /dev/null +++ b/PettingZoo/UI/Tab/Publisher/RawPublisherViewModel.cs @@ -0,0 +1,322 @@ +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 DelegateCommand publishCommand; + private readonly DelegateCommand propertiesExpandCollapseCommand; + private bool propertiesExpanded; + + private bool sendToExchange = true; + private string exchange = ""; + private string routingKey = ""; + private string queue = ""; + + private MessageDeliveryMode deliveryMode; + + private string contentType = "application/json"; + private string correlationId = ""; + private string replyTo = ""; + 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 bool SendToExchange + { + get => sendToExchange; + set => SetField(ref sendToExchange, value, otherPropertiesChanged: new[] { nameof(SendToQueue), nameof(ExchangeVisibility), nameof(QueueVisibility) }); + } + + + 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); + } + + + public string Queue + { + get => queue; + set => SetField(ref queue, value); + } + + + public virtual Visibility ExchangeVisibility => SendToExchange ? Visibility.Visible : Visibility.Collapsed; + public virtual Visibility QueueVisibility => SendToQueue ? Visibility.Visible : Visibility.Collapsed; + + + 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 ReplyTo + { + get => replyTo; + set => SetField(ref replyTo, 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) + { + this.connection = connection; + + publishCommand = new DelegateCommand(PublishExecute, PublishCanExecute); + propertiesExpandCollapseCommand = new DelegateCommand(PropertiesExpandCollapseExecute); + + AddHeader(); + } + + + 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 + // TODO support for Reply To to dynamic queue which waits for a message (or opens a new subscriber tab?) + + var headers = Headers.Where(h => h.IsValid()).ToDictionary(h => h.Key, h => h.Value); + + // TODO background worker / async + + connection.Publish(new PublishMessageInfo( + SendToExchange ? Exchange : "", + SendToExchange ? RoutingKey : Queue, + 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 = NullIfEmpty(ReplyTo), + 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!) + { + PropertiesExpanded = true; + + var capturedLastHeader = lastHeader; + capturedLastHeader.Key = "Example"; + capturedLastHeader.Value = "header"; + } + + + public override Visibility ExchangeVisibility => Visibility.Visible; + public override Visibility QueueVisibility => Visibility.Visible; + } +} diff --git a/PettingZoo/UI/Tab/Publisher/RawPublisherViewStrings.Designer.cs b/PettingZoo/UI/Tab/Publisher/RawPublisherViewStrings.Designer.cs new file mode 100644 index 0000000..3d74355 --- /dev/null +++ b/PettingZoo/UI/Tab/Publisher/RawPublisherViewStrings.Designer.cs @@ -0,0 +1,297 @@ +//------------------------------------------------------------------------------ +// +// 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 Exchange. + /// + public static string LabelExchange { + get { + return ResourceManager.GetString("LabelExchange", 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 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 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 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..4f8be42 --- /dev/null +++ b/PettingZoo/UI/Tab/Publisher/RawPublisherViewStrings.resx @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Exchange + + + Expiration + + + Headers + + + Message ID + + + Payload + + + Priority + + + Properties + + + Queue + + + Reply To + + + Routing key + + + Publish to exchange (topic) + + + Publish to queue (direct) + + + Timestamp + + + Type + + + User ID + + + ⏶ Collapse + + + ⏷ Expand + + \ No newline at end of file diff --git a/PettingZoo/UI/Tab/Subscriber/SubscriberView.xaml b/PettingZoo/UI/Tab/Subscriber/SubscriberView.xaml index c619885..7da2b71 100644 --- a/PettingZoo/UI/Tab/Subscriber/SubscriberView.xaml +++ b/PettingZoo/UI/Tab/Subscriber/SubscriberView.xaml @@ -28,8 +28,8 @@ - - + + diff --git a/PettingZoo/UI/Tab/Subscriber/SubscriberViewModel.cs b/PettingZoo/UI/Tab/Subscriber/SubscriberViewModel.cs index 1f695ba..64cc767 100644 --- a/PettingZoo/UI/Tab/Subscriber/SubscriberViewModel.cs +++ b/PettingZoo/UI/Tab/Subscriber/SubscriberViewModel.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using System.Windows.Input; using PettingZoo.Core.Connection; using PettingZoo.Core.Rendering; -using PettingZoo.RabbitMQ; // TODO update title with unread message count if tab is not active @@ -16,36 +15,36 @@ namespace PettingZoo.UI.Tab.Subscriber { private readonly ISubscriber subscriber; private readonly TaskScheduler uiScheduler; - private MessageInfo? selectedMessage; + private ReceivedMessageInfo? selectedMessage; private readonly DelegateCommand clearCommand; private readonly TabToolbarCommand[] toolbarCommands; + private IDictionary? selectedMessageProperties; public ICommand ClearCommand => clearCommand; - public ObservableCollection Messages { get; } + public ObservableCollection Messages { get; } - public MessageInfo? SelectedMessage + public ReceivedMessageInfo? SelectedMessage { get => selectedMessage; set { - if (value == selectedMessage) - return; - - selectedMessage = value; - RaisePropertyChanged(); - RaiseOtherPropertyChanged(nameof(SelectedMessageBody)); - RaiseOtherPropertyChanged(nameof(SelectedMessageProperties)); + if (SetField(ref selectedMessage, value, otherPropertiesChanged: new[] { nameof(SelectedMessageBody) })) + UpdateSelectedMessageProperties(); } } public string SelectedMessageBody => SelectedMessage != null - ? MessageBodyRenderer.Render(SelectedMessage.Body, SelectedMessage.Properties.ContentType()) + ? MessageBodyRenderer.Render(SelectedMessage.Body, SelectedMessage.Properties.ContentType) : ""; - public IDictionary? SelectedMessageProperties => SelectedMessage?.Properties; + public IDictionary? SelectedMessageProperties + { + get => selectedMessageProperties; + set => SetField(ref selectedMessageProperties, value); + } public string Title => $"{subscriber.Exchange} - {subscriber.RoutingKey}"; public IEnumerable ToolbarCommands => toolbarCommands; @@ -57,7 +56,7 @@ namespace PettingZoo.UI.Tab.Subscriber uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(); - Messages = new ObservableCollection(); + Messages = new ObservableCollection(); clearCommand = new DelegateCommand(ClearExecute, ClearCanExecute); toolbarCommands = new[] @@ -93,11 +92,18 @@ namespace PettingZoo.UI.Tab.Subscriber } + private void UpdateSelectedMessageProperties() + { + SelectedMessageProperties = SelectedMessage != null + ? MessagePropertiesRenderer.Render(SelectedMessage.Properties) + : null; + } + + private void RunFromUiScheduler(Action action) { _ = Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.None, uiScheduler); } - } @@ -116,9 +122,9 @@ namespace PettingZoo.UI.Tab.Subscriber } - public string Exchange { get; } = "dummy"; - public string RoutingKey { get; } = "dummy"; - + public string Exchange => "dummy"; + public string RoutingKey => "dummy"; + #pragma warning disable CS0067 public event EventHandler? MessageReceived; #pragma warning restore CS0067 From 057cac4e22e20f12279d2152aa2a530df5b6829e Mon Sep 17 00:00:00 2001 From: Mark van Renswoude Date: Wed, 15 Dec 2021 10:50:45 +0100 Subject: [PATCH 04/16] Upgraded to .NET 6 Implemented Tapeti compatible publishing WIP: example generator from Tapeti message classes (UI still hidden) --- .../MetadataReaderMessageParser.cs | 164 --------------- .../ParseTapetiMessagesPrototype.csproj | 12 -- ParseTapetiMessagesPrototype/Program.cs | 21 -- .../Generator/IExampleGenerator.cs | 25 +++ PettingZoo.Core/PettingZoo.Core.csproj | 2 +- .../PettingZoo.RabbitMQ.csproj | 2 +- PettingZoo.Tapeti/PettingZoo.Tapeti.csproj | 15 ++ .../TapetiClassLibraryExampleSource.cs | 187 +++++++++++++++++ .../TypeToJObjectConverter.cs | 83 ++------ PettingZoo.sln | 14 +- PettingZoo/PettingZoo.csproj | 27 ++- PettingZoo/Style.xaml | 3 +- .../UI/Example/ExamplePickerDialog.xaml | 12 ++ .../UI/Example/ExamplePickerDialog.xaml.cs | 15 ++ .../ExamplePickerDialogStrings.Designer.cs | 72 +++++++ .../Example/ExamplePickerDialogStrings.resx | 123 +++++++++++ .../Example/ExamplePickerDialogViewModel.cs | 6 + PettingZoo/UI/Main/MainWindow.xaml.cs | 3 +- .../UI/Tab/Publisher/PublisherView.xaml | 7 +- .../UI/Tab/Publisher/PublisherViewModel.cs | 11 +- .../PublisherViewStrings.Designer.cs | 6 +- .../Tab/Publisher/PublisherViewStrings.resx | 4 +- .../UI/Tab/Publisher/TapetiPublisherView.xaml | 78 +++++++ .../Tab/Publisher/TapetiPublisherView.xaml.cs | 26 +++ .../Tab/Publisher/TapetiPublisherViewModel.cs | 170 +++++++++++++++ .../TapetiPublisherViewStrings.Designer.cs | 198 ++++++++++++++++++ .../Publisher/TapetiPublisherViewStrings.resx | 165 +++++++++++++++ .../UI/Tab/Subscriber/SubscriberViewModel.cs | 2 + 28 files changed, 1165 insertions(+), 288 deletions(-) delete mode 100644 ParseTapetiMessagesPrototype/MetadataReaderMessageParser.cs delete mode 100644 ParseTapetiMessagesPrototype/ParseTapetiMessagesPrototype.csproj delete mode 100644 ParseTapetiMessagesPrototype/Program.cs create mode 100644 PettingZoo.Core/Generator/IExampleGenerator.cs create mode 100644 PettingZoo.Tapeti/PettingZoo.Tapeti.csproj create mode 100644 PettingZoo.Tapeti/TapetiClassLibraryExampleSource.cs rename ParseTapetiMessagesPrototype/AssemblyLoaderMessageParser.cs => PettingZoo.Tapeti/TypeToJObjectConverter.cs (50%) create mode 100644 PettingZoo/UI/Example/ExamplePickerDialog.xaml create mode 100644 PettingZoo/UI/Example/ExamplePickerDialog.xaml.cs create mode 100644 PettingZoo/UI/Example/ExamplePickerDialogStrings.Designer.cs create mode 100644 PettingZoo/UI/Example/ExamplePickerDialogStrings.resx create mode 100644 PettingZoo/UI/Example/ExamplePickerDialogViewModel.cs create mode 100644 PettingZoo/UI/Tab/Publisher/TapetiPublisherView.xaml create mode 100644 PettingZoo/UI/Tab/Publisher/TapetiPublisherView.xaml.cs create mode 100644 PettingZoo/UI/Tab/Publisher/TapetiPublisherViewModel.cs create mode 100644 PettingZoo/UI/Tab/Publisher/TapetiPublisherViewStrings.Designer.cs create mode 100644 PettingZoo/UI/Tab/Publisher/TapetiPublisherViewStrings.resx diff --git a/ParseTapetiMessagesPrototype/MetadataReaderMessageParser.cs b/ParseTapetiMessagesPrototype/MetadataReaderMessageParser.cs deleted file mode 100644 index f3081cb..0000000 --- a/ParseTapetiMessagesPrototype/MetadataReaderMessageParser.cs +++ /dev/null @@ -1,164 +0,0 @@ -using System; -using System.Collections.Immutable; -using System.IO; -using System.Reflection.Metadata; -using System.Reflection.PortableExecutable; -using System.Text; -using Newtonsoft.Json.Linq; - -namespace ParseTapetiMessagesPrototype -{ - public static class MetadataReaderMessageParser - { - public static void ParseAssembly(string classLibraryFilename) - { - try - { - using var fileStream = new FileStream(classLibraryFilename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - using var peReader = new PEReader(fileStream); - - var metadataReader = peReader.GetMetadataReader(); - - // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator - foreach (var typeDefinitionHandle in metadataReader.TypeDefinitions) - { - var typeDefinition = metadataReader.GetTypeDefinition(typeDefinitionHandle); - HandleTypeDefinition(metadataReader, typeDefinition); - } - } - catch (Exception e) - { - Console.WriteLine(e.Message); - } - } - - - private static void HandleTypeDefinition(MetadataReader metadataReader, TypeDefinition typeDefinition) - { - var typeNamespace = metadataReader.GetString(typeDefinition.Namespace); - var typeName = metadataReader.GetString(typeDefinition.Name); - - // For this prototype, filter out anything not ending in Message - // Might want to show a full tree in PettingZoo since this is just a convention - if (!typeName.EndsWith("Message")) - return; - - Console.WriteLine($"{typeNamespace}.{typeName}"); - - var example = new JObject(); - - // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator - foreach (var propertyDefinitionHandle in typeDefinition.GetProperties()) - { - // TODO get properties from base class - - var propertyDefinition = metadataReader.GetPropertyDefinition(propertyDefinitionHandle); - HandlePropertyDefinition(metadataReader, propertyDefinition, example); - } - - Console.WriteLine(example.ToString()); - Console.WriteLine(); - } - - private static void HandlePropertyDefinition(MetadataReader metadataReader, PropertyDefinition propertyDefinition, JObject targetObject) - { - var fieldName = metadataReader.GetString(propertyDefinition.Name); - var signature = propertyDefinition.DecodeSignature(new JsonSignatureProvider(), null); - - targetObject.Add(fieldName, signature.ReturnType); - } - - - private class JsonSignatureProvider : ISignatureTypeProvider - { - public JToken GetPrimitiveType(PrimitiveTypeCode typeCode) - { - return typeCode switch - { - PrimitiveTypeCode.Boolean => false, - - PrimitiveTypeCode.Byte or - PrimitiveTypeCode.Int16 or - PrimitiveTypeCode.Int32 or - PrimitiveTypeCode.Int64 or - PrimitiveTypeCode.IntPtr or - PrimitiveTypeCode.SByte or - PrimitiveTypeCode.UInt16 or - PrimitiveTypeCode.UInt32 or - PrimitiveTypeCode.UInt64 or - PrimitiveTypeCode.UIntPtr => 0, - - PrimitiveTypeCode.Char or - PrimitiveTypeCode.String => "", - - PrimitiveTypeCode.Double or - PrimitiveTypeCode.Single => 0.0, - - // TODO recurse - PrimitiveTypeCode.Object => "OBJECT", - - _ => $"Unsupported primitive type code: {typeCode}" - }; - } - - public JToken GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind = 0) => "typedef"; - - public JToken GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind = 0) - { - var typeReference = reader.GetTypeReference(handle); - var typeName = reader.GetString(typeReference.Name); - - return typeName; - } - - - public JToken GetTypeFromSpecification(MetadataReader reader, object genericContext, TypeSpecificationHandle handle, byte rawTypeKind = 0) => "typespec"; - - public JToken GetSZArrayType(JToken elementType) => new JValue(elementType + "[]"); - public JToken GetPointerType(JToken elementType) => null; - public JToken GetByReferenceType(JToken elementType) => null; - public JToken GetGenericMethodParameter(object genericContext, int index) => "!!" + index; - public JToken GetGenericTypeParameter(object genericContext, int index) => "!" + index; - - public JToken GetPinnedType(JToken elementType) => elementType + " pinned"; - public JToken GetGenericInstantiation(JToken genericType, ImmutableArray typeArguments) => genericType + "<" + string.Join(",", typeArguments) + ">"; - public JToken GetModifiedType(JToken modifierType, JToken unmodifiedType, bool isRequired) => unmodifiedType + (isRequired ? " modreq(" : " modopt(") + modifierType + ")"; - - public JToken GetArrayType(JToken elementType, ArrayShape shape) - { - var builder = new StringBuilder(); - - builder.Append(elementType); - builder.Append('['); - - for (int i = 0; i < shape.Rank; i++) - { - int lowerBound = 0; - - if (i < shape.LowerBounds.Length) - { - lowerBound = shape.LowerBounds[i]; - builder.Append(lowerBound); - } - - builder.Append("..."); - - if (i < shape.Sizes.Length) - { - builder.Append(lowerBound + shape.Sizes[i] - 1); - } - - if (i < shape.Rank - 1) - { - builder.Append(','); - } - } - - builder.Append(']'); - return builder.ToString(); - } - - public JToken GetFunctionPointerType(MethodSignature signature) => "methodptr(something)"; - } - } -} diff --git a/ParseTapetiMessagesPrototype/ParseTapetiMessagesPrototype.csproj b/ParseTapetiMessagesPrototype/ParseTapetiMessagesPrototype.csproj deleted file mode 100644 index b84e63f..0000000 --- a/ParseTapetiMessagesPrototype/ParseTapetiMessagesPrototype.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - Exe - net5.0 - - - - - - - diff --git a/ParseTapetiMessagesPrototype/Program.cs b/ParseTapetiMessagesPrototype/Program.cs deleted file mode 100644 index 9b753ce..0000000 --- a/ParseTapetiMessagesPrototype/Program.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace ParseTapetiMessagesPrototype -{ - public class Program - { - public static void Main() - { - const string classLibraryFilename = "D:\\Temp\\lib\\netstandard2.0\\Messaging.Relatie.dll"; - - // There are advantages to using the MetadataReader, for example no code is run (LoadAssemblyForReflection is no longer - // supported in .NET Core) and the assembly is not locked at all. This comes at the cost of complexity however, so - // this prototype explores both options. - // - // In the final version perhaps we can work around loading the assembly into our own process by spawning a new process - // to convert it into metadata used by the main process. - - //MetadataReaderMessageParser.ParseAssembly(classLibraryFilename); - - AssemblyLoaderMessageParser.ParseAssembly(classLibraryFilename); - } - } -} 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 index b380728..483d761 100644 --- a/PettingZoo.Core/PettingZoo.Core.csproj +++ b/PettingZoo.Core/PettingZoo.Core.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 enable diff --git a/PettingZoo.RabbitMQ/PettingZoo.RabbitMQ.csproj b/PettingZoo.RabbitMQ/PettingZoo.RabbitMQ.csproj index 38e4451..79d0e3f 100644 --- a/PettingZoo.RabbitMQ/PettingZoo.RabbitMQ.csproj +++ b/PettingZoo.RabbitMQ/PettingZoo.RabbitMQ.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 enable diff --git a/PettingZoo.Tapeti/PettingZoo.Tapeti.csproj b/PettingZoo.Tapeti/PettingZoo.Tapeti.csproj new file mode 100644 index 0000000..77a196f --- /dev/null +++ b/PettingZoo.Tapeti/PettingZoo.Tapeti.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + + + + + + + + + + + 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/ParseTapetiMessagesPrototype/AssemblyLoaderMessageParser.cs b/PettingZoo.Tapeti/TypeToJObjectConverter.cs similarity index 50% rename from ParseTapetiMessagesPrototype/AssemblyLoaderMessageParser.cs rename to PettingZoo.Tapeti/TypeToJObjectConverter.cs index 1bfbc66..d807501 100644 --- a/ParseTapetiMessagesPrototype/AssemblyLoaderMessageParser.cs +++ b/PettingZoo.Tapeti/TypeToJObjectConverter.cs @@ -2,66 +2,21 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Runtime.Loader; using Newtonsoft.Json.Linq; -namespace ParseTapetiMessagesPrototype +namespace PettingZoo.Tapeti { - public static class AssemblyLoaderMessageParser + internal class TypeToJObjectConverter { - public static void ParseAssembly(string classLibraryFilename) - { - var loadContext = new AssemblyLoadContext(null, true); - try - { - var assembly = loadContext.LoadFromAssemblyPath(classLibraryFilename); - - foreach (var assemblyType in assembly.GetTypes()) - HandleType(assemblyType); - } - finally - { - loadContext.Unload(); - } - } - - - private static void HandleType(Type type) - { - if (!type.IsClass) - return; - - // For this prototype, filter out anything not ending in Message - // Might want to show a full tree in PettingZoo since this is just a convention - if (!type.Name.EndsWith("Message") || type.Name != "RelatieUpdateMessage") - return; - - Console.WriteLine($"{type.Namespace}.{type.Name}"); - - // 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 slightly 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. - var serialized = TypeToJObject(type); - - Console.WriteLine(serialized); - Console.WriteLine(""); - } - - - private static JObject TypeToJObject(Type type) + 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); } @@ -82,38 +37,38 @@ namespace ParseTapetiMessagesPrototype { 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(TypeToJObject(enumerableInterface.GetGenericArguments()[0])); - - - return TypeToJObject(actualType); + return new JArray(Convert(enumerableInterface.GetGenericArguments()[0])); + + + return Convert(actualType); } if (actualType.IsArray) - return new JArray(TypeToJObject(actualType.GetElementType())); - + return new JArray(Convert(actualType.GetElementType())); + if (actualType.IsEnum) return Enum.GetNames(actualType).FirstOrDefault(); - + // Special cases for runtime generated values if (actualType == typeof(DateTime)) { @@ -129,5 +84,7 @@ namespace ParseTapetiMessagesPrototype ? mappedToken : $"(unknown type: {actualType.Name})"; } + + } } diff --git a/PettingZoo.sln b/PettingZoo.sln index f4d15a5..a8028f7 100644 --- a/PettingZoo.sln +++ b/PettingZoo.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31911.196 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31912.275 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo", "PettingZoo\PettingZoo.csproj", "{24819D09-C747-4356-B686-D9DE9CAA6F59}" EndProject @@ -14,7 +14,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.Core", "PettingZ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.RabbitMQ", "PettingZoo.RabbitMQ\PettingZoo.RabbitMQ.csproj", "{220149F3-A8D6-44ED-B3B6-DFE506EB018A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParseTapetiMessagesPrototype", "ParseTapetiMessagesPrototype\ParseTapetiMessagesPrototype.csproj", "{B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.Tapeti", "PettingZoo.Tapeti\PettingZoo.Tapeti.csproj", "{1763AB04-59D9-4663-B207-D6302FFAACD5}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -34,10 +34,10 @@ Global {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 - {B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}.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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/PettingZoo/PettingZoo.csproj b/PettingZoo/PettingZoo.csproj index cd97af0..fa44a96 100644 --- a/PettingZoo/PettingZoo.csproj +++ b/PettingZoo/PettingZoo.csproj @@ -2,7 +2,7 @@ WinExe - net5.0-windows + net6.0-windows true Mark van Renswoude Petting Zoo @@ -46,6 +46,11 @@ True True + + True + True + ExamplePickerDialogStrings.resx + MainWindowStrings.resx True @@ -56,6 +61,14 @@ True True + + TapetiPublisherViewStrings.resx + True + True + + + Code + RawPublisherViewStrings.resx True @@ -78,6 +91,10 @@ ConnectionWindowStrings.Designer.cs PublicResXFileCodeGenerator + + PublicResXFileCodeGenerator + ExamplePickerDialogStrings.Designer.cs + MainWindowStrings.Designer.cs PublicResXFileCodeGenerator @@ -86,6 +103,10 @@ SubscribeWindowStrings.Designer.cs PublicResXFileCodeGenerator + + TapetiPublisherViewStrings.Designer.cs + PublicResXFileCodeGenerator + RawPublisherViewStrings.Designer.cs PublicResXFileCodeGenerator @@ -113,6 +134,10 @@ $(DefaultXamlRuntime) + + $(DefaultXamlRuntime) + Designer + $(DefaultXamlRuntime) diff --git a/PettingZoo/Style.xaml b/PettingZoo/Style.xaml index 5f5e226..da8e0fe 100644 --- a/PettingZoo/Style.xaml +++ b/PettingZoo/Style.xaml @@ -67,8 +67,9 @@ - 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/PettingZoo/UI/Main/MainWindow.xaml.cs b/PettingZoo/UI/Main/MainWindow.xaml.cs index 2f033ff..c41d85e 100644 --- a/PettingZoo/UI/Main/MainWindow.xaml.cs +++ b/PettingZoo/UI/Main/MainWindow.xaml.cs @@ -37,8 +37,7 @@ namespace PettingZoo.UI.Main private void MainWindow_OnLoaded(object sender, RoutedEventArgs e) { - // TODO support command-line parameters for easier testing - //viewModel.ConnectCommand.Execute(null); + viewModel.ConnectCommand.Execute(null); } diff --git a/PettingZoo/UI/Tab/Publisher/PublisherView.xaml b/PettingZoo/UI/Tab/Publisher/PublisherView.xaml index accf7fb..d26cc84 100644 --- a/PettingZoo/UI/Tab/Publisher/PublisherView.xaml +++ b/PettingZoo/UI/Tab/Publisher/PublisherView.xaml @@ -15,10 +15,9 @@ - -
public partial class RawPublisherView { - private RawPublisherViewModel viewModel; - private DispatcherTimer checkEmptyHeaderTimer; + private readonly RawPublisherViewModel viewModel; + private readonly DispatcherTimer checkEmptyHeaderTimer; public RawPublisherView(RawPublisherViewModel viewModel) { this.viewModel = viewModel; - InitializeComponent(); DataContext = viewModel; + InitializeComponent(); checkEmptyHeaderTimer = new DispatcherTimer(); checkEmptyHeaderTimer.Tick += CheckEmptyHeaderTimerOnTick; diff --git a/PettingZoo/UI/Tab/Publisher/RawPublisherViewModel.cs b/PettingZoo/UI/Tab/Publisher/RawPublisherViewModel.cs index 33e90ae..e163462 100644 --- a/PettingZoo/UI/Tab/Publisher/RawPublisherViewModel.cs +++ b/PettingZoo/UI/Tab/Publisher/RawPublisherViewModel.cs @@ -13,20 +13,15 @@ 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 bool sendToExchange = true; - private string exchange = ""; - private string routingKey = ""; - private string queue = ""; - private MessageDeliveryMode deliveryMode; private string contentType = "application/json"; private string correlationId = ""; - private string replyTo = ""; private string appId = ""; private string contentEncoding = ""; private string expiration = ""; @@ -38,44 +33,6 @@ namespace PettingZoo.UI.Tab.Publisher private string payload = ""; - public bool SendToExchange - { - get => sendToExchange; - set => SetField(ref sendToExchange, value, otherPropertiesChanged: new[] { nameof(SendToQueue), nameof(ExchangeVisibility), nameof(QueueVisibility) }); - } - - - 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); - } - - - public string Queue - { - get => queue; - set => SetField(ref queue, value); - } - - - public virtual Visibility ExchangeVisibility => SendToExchange ? Visibility.Visible : Visibility.Collapsed; - public virtual Visibility QueueVisibility => SendToQueue ? Visibility.Visible : Visibility.Collapsed; - public int DeliveryModeIndex { @@ -98,13 +55,6 @@ namespace PettingZoo.UI.Tab.Publisher } - public string ReplyTo - { - get => replyTo; - set => SetField(ref replyTo, value); - } - - public string AppId { get => appId; @@ -194,20 +144,17 @@ namespace PettingZoo.UI.Tab.Publisher protected Header LastHeader; - public RawPublisherViewModel(IConnection connection, ReceivedMessageInfo? receivedMessage = null) + 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) { - Exchange = receivedMessage.Exchange; - RoutingKey = receivedMessage.RoutingKey; - CorrelationId = receivedMessage.Properties.CorrelationId ?? ""; - ReplyTo = receivedMessage.Properties.ReplyTo ?? ""; Priority = receivedMessage.Properties.Priority?.ToString() ?? ""; AppId = receivedMessage.Properties.AppId ?? ""; ContentEncoding = receivedMessage.Properties.ContentEncoding ?? ""; @@ -270,15 +217,14 @@ namespace PettingZoo.UI.Tab.Publisher } // TODO check parsing of priority and timestamp - // TODO support for Reply To to dynamic queue which waits for a message (or opens a new subscriber tab?) - + var headers = Headers.Where(h => h.IsValid()).ToDictionary(h => h.Key, h => h.Value); // TODO background worker / async connection.Publish(new PublishMessageInfo( - SendToExchange ? Exchange : "", - SendToExchange ? RoutingKey : Queue, + publishDestination.Exchange, + publishDestination.RoutingKey, Encoding.UTF8.GetBytes(Payload), new MessageProperties(headers) { @@ -290,7 +236,7 @@ namespace PettingZoo.UI.Tab.Publisher Expiration = NullIfEmpty(Expiration), MessageId = NullIfEmpty(MessageId), Priority = !string.IsNullOrEmpty(Priority) && byte.TryParse(Priority, out var priorityValue) ? priorityValue : null, - ReplyTo = NullIfEmpty(ReplyTo), + ReplyTo = publishDestination.GetReplyTo(), Timestamp = !string.IsNullOrEmpty(Timestamp) && DateTime.TryParse(Timestamp, out var timestampValue) ? timestampValue : null, Type = NullIfEmpty(TypeProperty), UserId = NullIfEmpty(UserId) @@ -341,7 +287,7 @@ namespace PettingZoo.UI.Tab.Publisher public class DesignTimeRawPublisherViewModel : RawPublisherViewModel { - public DesignTimeRawPublisherViewModel() : base(null!) + public DesignTimeRawPublisherViewModel() : base(null!, null!) { PropertiesExpanded = true; @@ -349,9 +295,5 @@ namespace PettingZoo.UI.Tab.Publisher capturedLastHeader.Key = "Example"; capturedLastHeader.Value = "header"; } - - - public override Visibility ExchangeVisibility => Visibility.Visible; - public override Visibility QueueVisibility => Visibility.Visible; } } diff --git a/PettingZoo/UI/Tab/Publisher/RawPublisherViewStrings.Designer.cs b/PettingZoo/UI/Tab/Publisher/RawPublisherViewStrings.Designer.cs index 3d74355..e771a60 100644 --- a/PettingZoo/UI/Tab/Publisher/RawPublisherViewStrings.Designer.cs +++ b/PettingZoo/UI/Tab/Publisher/RawPublisherViewStrings.Designer.cs @@ -141,15 +141,6 @@ namespace PettingZoo.UI.Tab.Publisher { } } - /// - /// 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 Expiration. /// @@ -204,51 +195,6 @@ namespace PettingZoo.UI.Tab.Publisher { } } - /// - /// 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 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 Timestamp. /// diff --git a/PettingZoo/UI/Tab/Publisher/RawPublisherViewStrings.resx b/PettingZoo/UI/Tab/Publisher/RawPublisherViewStrings.resx index 4f8be42..628f2d6 100644 --- a/PettingZoo/UI/Tab/Publisher/RawPublisherViewStrings.resx +++ b/PettingZoo/UI/Tab/Publisher/RawPublisherViewStrings.resx @@ -144,9 +144,6 @@ Delivery mode - - Exchange - Expiration @@ -165,21 +162,6 @@ Properties - - Queue - - - Reply To - - - Routing key - - - Publish to exchange (topic) - - - Publish to queue (direct) - Timestamp diff --git a/PettingZoo/UI/Tab/Publisher/TapetiPublisherView.xaml b/PettingZoo/UI/Tab/Publisher/TapetiPublisherView.xaml index ebd9c3e..bda2de9 100644 --- a/PettingZoo/UI/Tab/Publisher/TapetiPublisherView.xaml +++ b/PettingZoo/UI/Tab/Publisher/TapetiPublisherView.xaml @@ -11,17 +11,11 @@ Background="White"> - + - - - - - - @@ -32,47 +26,28 @@ - + diff --git a/PettingZoo/UI/Tab/Publisher/TapetiPublisherView.xaml.cs b/PettingZoo/UI/Tab/Publisher/TapetiPublisherView.xaml.cs index dc6ba17..bde91cb 100644 --- a/PettingZoo/UI/Tab/Publisher/TapetiPublisherView.xaml.cs +++ b/PettingZoo/UI/Tab/Publisher/TapetiPublisherView.xaml.cs @@ -10,8 +10,8 @@ namespace PettingZoo.UI.Tab.Publisher { public TapetiPublisherView(TapetiPublisherViewModel viewModel) { - InitializeComponent(); DataContext = viewModel; + InitializeComponent(); } @@ -20,7 +20,7 @@ namespace PettingZoo.UI.Tab.Publisher if (sender is not TextBox textBox) return; - textBox.CaretIndex = textBox.Text?.Length ?? 0; + textBox.CaretIndex = textBox.Text.Length; } } } diff --git a/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewModel.cs b/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewModel.cs index a4de74f..6c04620 100644 --- a/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewModel.cs +++ b/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewModel.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Text; -using System.Windows; using System.Windows.Input; using PettingZoo.Core.Connection; using IConnection = PettingZoo.Core.Connection.IConnection; @@ -10,68 +9,15 @@ namespace PettingZoo.UI.Tab.Publisher public class TapetiPublisherViewModel : BaseViewModel { private readonly IConnection connection; + private readonly IPublishDestination publishDestination; private readonly DelegateCommand publishCommand; - private bool sendToExchange = true; - private string exchange = ""; - private string routingKey = ""; - private string queue = ""; - - private MessageDeliveryMode deliveryMode; - private string correlationId = ""; - private string replyTo = ""; private string payload = ""; private string className = ""; private string assemblyName = ""; - public bool SendToExchange - { - get => sendToExchange; - set => SetField(ref sendToExchange, value, otherPropertiesChanged: new[] { nameof(SendToQueue), nameof(ExchangeVisibility), nameof(QueueVisibility) }); - } - - - 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); - } - - - public string Queue - { - get => queue; - set => SetField(ref queue, value); - } - - - public virtual Visibility ExchangeVisibility => SendToExchange ? Visibility.Visible : Visibility.Collapsed; - public virtual Visibility QueueVisibility => SendToQueue ? Visibility.Visible : Visibility.Collapsed; - - - public int DeliveryModeIndex - { - get => deliveryMode == MessageDeliveryMode.Persistent ? 1 : 0; - set => SetField(ref deliveryMode, value == 1 ? MessageDeliveryMode.Persistent : MessageDeliveryMode.NonPersistent); - } - - public string CorrelationId { get => correlationId; @@ -79,13 +25,6 @@ namespace PettingZoo.UI.Tab.Publisher } - public string ReplyTo - { - get => replyTo; - set => SetField(ref replyTo, value); - } - - public string ClassName { get => string.IsNullOrEmpty(className) ? AssemblyName + "." : className; @@ -142,9 +81,10 @@ namespace PettingZoo.UI.Tab.Publisher } - public TapetiPublisherViewModel(IConnection connection, ReceivedMessageInfo? receivedMessage = null) + public TapetiPublisherViewModel(IConnection connection, IPublishDestination publishDestination, ReceivedMessageInfo? receivedMessage = null) { this.connection = connection; + this.publishDestination = publishDestination; publishCommand = new DelegateCommand(PublishExecute, PublishCanExecute); @@ -152,14 +92,9 @@ namespace PettingZoo.UI.Tab.Publisher if (receivedMessage == null || !IsTapetiMessage(receivedMessage, out var receivedAssemblyName, out var receivedClassName)) return; - Exchange = receivedMessage.Exchange; - RoutingKey = receivedMessage.RoutingKey; - - AssemblyName = receivedAssemblyName; ClassName = receivedClassName; CorrelationId = receivedMessage.Properties.CorrelationId ?? ""; - ReplyTo = receivedMessage.Properties.ReplyTo ?? ""; Payload = Encoding.UTF8.GetString(receivedMessage.Body); } @@ -171,12 +106,11 @@ namespace PettingZoo.UI.Tab.Publisher return string.IsNullOrEmpty(value) ? null : value; } - // TODO support for Reply To to dynamic queue which waits for a message (or opens a new subscriber tab?) // TODO background worker / async connection.Publish(new PublishMessageInfo( - SendToExchange ? Exchange : "", - SendToExchange ? RoutingKey : Queue, + publishDestination.Exchange, + publishDestination.RoutingKey, Encoding.UTF8.GetBytes(Payload), new MessageProperties(new Dictionary { @@ -185,8 +119,8 @@ namespace PettingZoo.UI.Tab.Publisher { ContentType = @"application/json", CorrelationId = NullIfEmpty(CorrelationId), - DeliveryMode = deliveryMode, - ReplyTo = NullIfEmpty(ReplyTo) + DeliveryMode = MessageDeliveryMode.Persistent, + ReplyTo = publishDestination.GetReplyTo() })); } @@ -201,12 +135,12 @@ namespace PettingZoo.UI.Tab.Publisher public class DesignTimeTapetiPublisherViewModel : TapetiPublisherViewModel { - public DesignTimeTapetiPublisherViewModel() : base(null!) + 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}"; } - - - public override Visibility ExchangeVisibility => Visibility.Visible; - public override Visibility QueueVisibility => Visibility.Visible; } } diff --git a/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewStrings.Designer.cs b/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewStrings.Designer.cs index ad3d777..4aa5b28 100644 --- a/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewStrings.Designer.cs +++ b/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewStrings.Designer.cs @@ -123,15 +123,6 @@ namespace PettingZoo.UI.Tab.Publisher { } } - /// - /// 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 Payload. /// @@ -149,50 +140,5 @@ namespace PettingZoo.UI.Tab.Publisher { return ResourceManager.GetString("LabelProperties", 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 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); - } - } } } diff --git a/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewStrings.resx b/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewStrings.resx index 1616460..7cf5efb 100644 --- a/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewStrings.resx +++ b/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewStrings.resx @@ -138,28 +138,10 @@ Delivery mode - - Exchange - Payload Properties - - Queue - - - Reply To - - - Routing key - - - Publish to exchange (topic) - - - Publish to queue (direct) - \ No newline at end of file diff --git a/PettingZoo/UI/Tab/Subscriber/SubscriberView.xaml.cs b/PettingZoo/UI/Tab/Subscriber/SubscriberView.xaml.cs index e0f6ac8..e0220f8 100644 --- a/PettingZoo/UI/Tab/Subscriber/SubscriberView.xaml.cs +++ b/PettingZoo/UI/Tab/Subscriber/SubscriberView.xaml.cs @@ -10,8 +10,8 @@ namespace PettingZoo.UI.Tab.Subscriber { public SubscriberView(SubscriberViewModel viewModel) { - InitializeComponent(); DataContext = viewModel; + InitializeComponent(); if (!System.ComponentModel.DesignerProperties.GetIsInDesignMode(this)) diff --git a/PettingZoo/UI/Tab/Subscriber/SubscriberViewModel.cs b/PettingZoo/UI/Tab/Subscriber/SubscriberViewModel.cs index f59bd5a..b4768e6 100644 --- a/PettingZoo/UI/Tab/Subscriber/SubscriberViewModel.cs +++ b/PettingZoo/UI/Tab/Subscriber/SubscriberViewModel.cs @@ -7,12 +7,11 @@ using System.Windows.Input; using PettingZoo.Core.Connection; using PettingZoo.Core.Rendering; -// TODO update title with unread message count if tab is not active -// TODO export option (to Tapeti.Cmd compatible format / command-line of course) +// 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 + public class SubscriberViewModel : BaseViewModel, ITabToolbarCommands, ITabActivate { private readonly ITabHost tabHost; private readonly ITabFactory tabFactory; @@ -26,8 +25,13 @@ namespace PettingZoo.UI.Tab.Subscriber 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; } @@ -53,7 +57,9 @@ namespace PettingZoo.UI.Tab.Subscriber set => SetField(ref selectedMessageProperties, value); } - public string Title => $"{subscriber.Exchange} - {subscriber.RoutingKey}"; + public string Title => + (subscriber.Exchange != null ? $"{subscriber.Exchange} - {subscriber.RoutingKey}" : $"{subscriber.QueueName}") + + (tabActive || unreadCount == 0 ? "" : $" ({unreadCount})"); public IEnumerable ToolbarCommands => toolbarCommands; @@ -111,6 +117,12 @@ namespace PettingZoo.UI.Tab.Subscriber { RunFromUiScheduler(() => { + if (!tabActive) + { + unreadCount++; + RaisePropertyChanged(nameof(Title)); + } + Messages.Add(args.MessageInfo); clearCommand.RaiseCanExecuteChanged(); }); @@ -131,6 +143,20 @@ namespace PettingZoo.UI.Tab.Subscriber { _ = Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.None, uiScheduler); } + + + public void Activate() + { + tabActive = true; + unreadCount = 0; + + RaisePropertyChanged(nameof(Title)); + } + + public void Deactivate() + { + tabActive = false; + } } @@ -149,6 +175,7 @@ namespace PettingZoo.UI.Tab.Subscriber } + public string QueueName => "dummy"; public string Exchange => "dummy"; public string RoutingKey => "dummy"; diff --git a/PettingZoo/UI/Tab/ViewTab.cs b/PettingZoo/UI/Tab/ViewTab.cs index 470d16d..9ec90b9 100644 --- a/PettingZoo/UI/Tab/ViewTab.cs +++ b/PettingZoo/UI/Tab/ViewTab.cs @@ -47,5 +47,17 @@ namespace PettingZoo.UI.Tab 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 index ae5829a..608d835 100644 --- a/PettingZoo/UI/Tab/ViewTabFactory.cs +++ b/PettingZoo/UI/Tab/ViewTabFactory.cs @@ -31,7 +31,7 @@ namespace PettingZoo.UI.Tab public ITab CreatePublisherTab(IConnection connection, ReceivedMessageInfo? fromReceivedMessage = null) { - var viewModel = new PublisherViewModel(connection, fromReceivedMessage); + var viewModel = new PublisherViewModel(tabHost, this, connection, fromReceivedMessage); return new ViewTab( closeTabCommand, new PublisherView(viewModel), From c9636aff0461a8c8d0be9cf5ec6c254768094fbb Mon Sep 17 00:00:00 2001 From: Mark van Renswoude Date: Sat, 18 Dec 2021 14:02:55 +0100 Subject: [PATCH 10/16] Implemented tab reordering Implemented tab undocking / docking --- PettingZoo/Images/Dock.svg | 50 +++++++ PettingZoo/Images/Undock.svg | 50 +++++++ PettingZoo/PettingZoo.csproj | 16 +++ PettingZoo/TODO.md | 3 +- .../UI/Connection/ConnectionViewModel.cs | 1 - PettingZoo/UI/Main/ITabContainer.cs | 8 ++ PettingZoo/UI/Main/MainWindow.xaml | 62 ++++++--- PettingZoo/UI/Main/MainWindow.xaml.cs | 77 ++++++++++- .../UI/Main/MainWindowStrings.Designer.cs | 29 ++++- PettingZoo/UI/Main/MainWindowStrings.resx | 9 ++ PettingZoo/UI/Main/MainWindowViewModel.cs | 81 ++++++++++-- PettingZoo/UI/Tab/ITab.cs | 1 - PettingZoo/UI/Tab/ITabHost.cs | 3 + .../UI/Tab/Publisher/PublisherView.xaml | 93 ++++++------- .../UI/Tab/Publisher/PublisherViewModel.cs | 2 - .../UndockedTabHostStrings.Designer.cs | 72 ++++++++++ .../Tab/Undocked/UndockedTabHostStrings.resx | 123 ++++++++++++++++++ .../Tab/Undocked/UndockedTabHostViewModel.cs | 85 ++++++++++++ .../Tab/Undocked/UndockedTabHostWindow.xaml | 44 +++++++ .../Undocked/UndockedTabHostWindow.xaml.cs | 34 +++++ PettingZoo/UI/Tab/ViewTab.cs | 4 +- PettingZoo/UI/Tab/ViewTabFactory.cs | 6 +- 22 files changed, 760 insertions(+), 93 deletions(-) create mode 100644 PettingZoo/Images/Dock.svg create mode 100644 PettingZoo/Images/Undock.svg create mode 100644 PettingZoo/UI/Main/ITabContainer.cs create mode 100644 PettingZoo/UI/Tab/Undocked/UndockedTabHostStrings.Designer.cs create mode 100644 PettingZoo/UI/Tab/Undocked/UndockedTabHostStrings.resx create mode 100644 PettingZoo/UI/Tab/Undocked/UndockedTabHostViewModel.cs create mode 100644 PettingZoo/UI/Tab/Undocked/UndockedTabHostWindow.xaml create mode 100644 PettingZoo/UI/Tab/Undocked/UndockedTabHostWindow.xaml.cs diff --git a/PettingZoo/Images/Dock.svg b/PettingZoo/Images/Dock.svg new file mode 100644 index 0000000..b1eabc4 --- /dev/null +++ b/PettingZoo/Images/Dock.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PettingZoo/Images/Undock.svg b/PettingZoo/Images/Undock.svg new file mode 100644 index 0000000..cdc1afb --- /dev/null +++ b/PettingZoo/Images/Undock.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PettingZoo/PettingZoo.csproj b/PettingZoo/PettingZoo.csproj index b227252..6e599ee 100644 --- a/PettingZoo/PettingZoo.csproj +++ b/PettingZoo/PettingZoo.csproj @@ -18,13 +18,16 @@ + + + @@ -42,6 +45,10 @@ + + + + True @@ -91,6 +98,11 @@ True True + + True + True + UndockedTabHostStrings.resx + @@ -130,6 +142,10 @@ SubscriberViewStrings.Designer.cs PublicResXFileCodeGenerator + + PublicResXFileCodeGenerator + UndockedTabHostStrings.Designer.cs + diff --git a/PettingZoo/TODO.md b/PettingZoo/TODO.md index c181109..450c51a 100644 --- a/PettingZoo/TODO.md +++ b/PettingZoo/TODO.md @@ -1,11 +1,10 @@ Must-have --------- +- Option to not save password in profiles Should-have ----------- -- Support undocking tabs (and redocking afterwards) -- Allow tab reordering - Save / load publisher messages (either as templates or to disk) - Export received messages to Tapeti JSON file / Tapeti.Cmd command-line diff --git a/PettingZoo/UI/Connection/ConnectionViewModel.cs b/PettingZoo/UI/Connection/ConnectionViewModel.cs index 55a02b8..37afe6e 100644 --- a/PettingZoo/UI/Connection/ConnectionViewModel.cs +++ b/PettingZoo/UI/Connection/ConnectionViewModel.cs @@ -235,7 +235,6 @@ namespace PettingZoo.UI.Connection private bool SaveCanExecute() { - // TODO check changes in parameters (excluding password) return SelectedStoredConnection != null && SelectedStoredConnection.Id != Guid.Empty && ValidConnection(false) && 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 index 6194a98..b0fa1a7 100644 --- a/PettingZoo/UI/Main/MainWindow.xaml +++ b/PettingZoo/UI/Main/MainWindow.xaml @@ -6,6 +6,7 @@ xmlns:main="clr-namespace:PettingZoo.UI.Main" xmlns:tab="clr-namespace:PettingZoo.UI.Tab" xmlns:svgc="http://sharpvectors.codeplex.com/svgc/" + xmlns:ui="clr-namespace:PettingZoo.UI" mc:Ignorable="d" d:DataContext="{d:DesignInstance main:DesignTimeMainWindowViewModel, IsDesignTimeCreatable=True}" Width="800" @@ -17,7 +18,11 @@ Closed="MainWindow_OnClosed"> + + + + + @@ -70,28 +81,39 @@ - + - - - - - - - - - - + + + + + + + + + + + + diff --git a/PettingZoo/UI/Main/MainWindow.xaml.cs b/PettingZoo/UI/Main/MainWindow.xaml.cs index 10221a7..ea1b01d 100644 --- a/PettingZoo/UI/Main/MainWindow.xaml.cs +++ b/PettingZoo/UI/Main/MainWindow.xaml.cs @@ -1,15 +1,20 @@ 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; // TODO improve readability of the connection status (especially when connecting/disconnected) namespace PettingZoo.UI.Main { #pragma warning disable CA1001 // MainWindow can't be IDisposable, handled instead in OnDispatcherShutDownStarted - public partial class MainWindow + public partial class MainWindow : ITabContainer { private readonly MainWindowViewModel viewModel; @@ -18,7 +23,7 @@ namespace PettingZoo.UI.Main { WindowStartupLocation = WindowStartupLocation.CenterScreen; - viewModel = new MainWindowViewModel(connectionFactory, connectionDialog, subscribeDialog); + viewModel = new MainWindowViewModel(connectionFactory, connectionDialog, subscribeDialog, this); DataContext = viewModel; InitializeComponent(); @@ -43,6 +48,74 @@ namespace PettingZoo.UI.Main { 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; } #pragma warning restore CA1001 } diff --git a/PettingZoo/UI/Main/MainWindowStrings.Designer.cs b/PettingZoo/UI/Main/MainWindowStrings.Designer.cs index 8b3f52f..a167f44 100644 --- a/PettingZoo/UI/Main/MainWindowStrings.Designer.cs +++ b/PettingZoo/UI/Main/MainWindowStrings.Designer.cs @@ -19,7 +19,7 @@ namespace PettingZoo.UI.Main { // 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.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class MainWindowStrings { @@ -96,6 +96,15 @@ namespace PettingZoo.UI.Main { } } + /// + /// Looks up a localized string similar to Undock tab. + /// + public static string CommandUndock { + get { + return ResourceManager.GetString("CommandUndock", resourceCulture); + } + } + /// /// Looks up a localized string similar to Close tab. /// @@ -105,6 +114,15 @@ namespace PettingZoo.UI.Main { } } + /// + /// Looks up a localized string similar to Undock. + /// + public static string ContextMenuUndockTab { + get { + return ResourceManager.GetString("ContextMenuUndockTab", resourceCulture); + } + } + /// /// Looks up a localized string similar to Non-persistent. /// @@ -159,6 +177,15 @@ namespace PettingZoo.UI.Main { } } + /// + /// 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. /// diff --git a/PettingZoo/UI/Main/MainWindowStrings.resx b/PettingZoo/UI/Main/MainWindowStrings.resx index 6df2e25..6814dea 100644 --- a/PettingZoo/UI/Main/MainWindowStrings.resx +++ b/PettingZoo/UI/Main/MainWindowStrings.resx @@ -129,9 +129,15 @@ New Subscriber... + + Undock tab + Close tab + + Undock + Non-persistent @@ -150,6 +156,9 @@ Error: {0} + + No open tabs. Click on New Publisher or New Subscriber to open a new tab. + Petting Zoo - a RabbitMQ live message viewer diff --git a/PettingZoo/UI/Main/MainWindowViewModel.cs b/PettingZoo/UI/Main/MainWindowViewModel.cs index 5da1cdc..b16d70e 100644 --- a/PettingZoo/UI/Main/MainWindowViewModel.cs +++ b/PettingZoo/UI/Main/MainWindowViewModel.cs @@ -9,6 +9,7 @@ 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 { @@ -17,18 +18,21 @@ namespace PettingZoo.UI.Main 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; public string ConnectionStatus @@ -60,6 +64,7 @@ namespace PettingZoo.UI.Main 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 @@ -68,13 +73,17 @@ namespace PettingZoo.UI.Main 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) + ISubscribeDialog subscribeDialog, ITabContainer tabContainer) { this.connectionFactory = connectionFactory; this.connectionDialog = connectionDialog; this.subscribeDialog = subscribeDialog; + this.tabContainer = tabContainer; connectionStatus = GetConnectionStatus(null); @@ -83,9 +92,10 @@ namespace PettingZoo.UI.Main disconnectCommand = new DelegateCommand(DisconnectExecute, IsConnectedCanExecute); publishCommand = new DelegateCommand(PublishExecute, IsConnectedCanExecute); subscribeCommand = new DelegateCommand(SubscribeExecute, IsConnectedCanExecute); - closeTabCommand = new DelegateCommand(CloseTabExecute, CloseTabCanExecute); + closeTabCommand = new DelegateCommand(CloseTabExecute, HasActiveTabCanExecute); + undockTabCommand = new DelegateCommand(UndockTabExecute, HasActiveTabCanExecute); - tabFactory = new ViewTabFactory(this, closeTabCommand); + tabFactory = new ViewTabFactory(this); } @@ -125,7 +135,16 @@ namespace PettingZoo.UI.Main 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(); @@ -170,24 +189,48 @@ namespace PettingZoo.UI.Main private void CloseTabExecute() { - if (ActiveTab == null) + 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; + 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 CloseTabCanExecute() + + private bool HasActiveTabCanExecute() { return ActiveTab != null; } @@ -199,9 +242,25 @@ namespace PettingZoo.UI.Main 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(); @@ -232,7 +291,7 @@ namespace PettingZoo.UI.Main public class DesignTimeMainWindowViewModel : MainWindowViewModel { - public DesignTimeMainWindowViewModel() : base(null!, null!, null!) + public DesignTimeMainWindowViewModel() : base(null!, null!, null!, null!) { } } diff --git a/PettingZoo/UI/Tab/ITab.cs b/PettingZoo/UI/Tab/ITab.cs index 5e650f7..2448b1f 100644 --- a/PettingZoo/UI/Tab/ITab.cs +++ b/PettingZoo/UI/Tab/ITab.cs @@ -23,7 +23,6 @@ namespace PettingZoo.UI.Tab { string Title { get; } ContentControl Content { get; } - ICommand CloseTabCommand { get; } } diff --git a/PettingZoo/UI/Tab/ITabHost.cs b/PettingZoo/UI/Tab/ITabHost.cs index ec570df..e73ec07 100644 --- a/PettingZoo/UI/Tab/ITabHost.cs +++ b/PettingZoo/UI/Tab/ITabHost.cs @@ -3,5 +3,8 @@ public interface ITabHost { void AddTab(ITab tab); + + void DockTab(ITab tab); + void UndockedTabClosed(ITab tab); } } diff --git a/PettingZoo/UI/Tab/Publisher/PublisherView.xaml b/PettingZoo/UI/Tab/Publisher/PublisherView.xaml index 81d25cc..394ed57 100644 --- a/PettingZoo/UI/Tab/Publisher/PublisherView.xaml +++ b/PettingZoo/UI/Tab/Publisher/PublisherView.xaml @@ -10,54 +10,57 @@ d:DesignWidth="800" d:DataContext="{d:DesignInstance res:DesignTimePublisherViewModel, IsDesignTimeCreatable=True}" Background="White"> - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + diff --git a/PettingZoo/UI/Tab/Undocked/UndockedTabHostWindow.xaml.cs b/PettingZoo/UI/Tab/Undocked/UndockedTabHostWindow.xaml.cs new file mode 100644 index 0000000..52ede6b --- /dev/null +++ b/PettingZoo/UI/Tab/Undocked/UndockedTabHostWindow.xaml.cs @@ -0,0 +1,34 @@ +using System.Windows; + +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(); + }; + } + } +} diff --git a/PettingZoo/UI/Tab/ViewTab.cs b/PettingZoo/UI/Tab/ViewTab.cs index 9ec90b9..22950dc 100644 --- a/PettingZoo/UI/Tab/ViewTab.cs +++ b/PettingZoo/UI/Tab/ViewTab.cs @@ -12,7 +12,6 @@ namespace PettingZoo.UI.Tab { public string Title => getTitle(viewModel); public ContentControl Content { get; } - public ICommand CloseTabCommand { get; } public IEnumerable ToolbarCommands => viewModel is ITabToolbarCommands tabToolbarCommands ? tabToolbarCommands.ToolbarCommands @@ -25,14 +24,13 @@ namespace PettingZoo.UI.Tab private readonly Func getTitle; - public ViewTab(ICommand closeTabCommand, TView view, TViewModel viewModel, Expression> title) + 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; - CloseTabCommand = closeTabCommand; this.viewModel = viewModel; getTitle = title.Compile(); Content = view; diff --git a/PettingZoo/UI/Tab/ViewTabFactory.cs b/PettingZoo/UI/Tab/ViewTabFactory.cs index 608d835..80d90dc 100644 --- a/PettingZoo/UI/Tab/ViewTabFactory.cs +++ b/PettingZoo/UI/Tab/ViewTabFactory.cs @@ -8,13 +8,11 @@ namespace PettingZoo.UI.Tab public class ViewTabFactory : ITabFactory { private readonly ITabHost tabHost; - private readonly ICommand closeTabCommand; - public ViewTabFactory(ITabHost tabHost, ICommand closeTabCommand) + public ViewTabFactory(ITabHost tabHost) { this.tabHost = tabHost; - this.closeTabCommand = closeTabCommand; } @@ -22,7 +20,6 @@ namespace PettingZoo.UI.Tab { var viewModel = new SubscriberViewModel(tabHost, this, connection, subscriber); return new ViewTab( - closeTabCommand, new SubscriberView(viewModel), viewModel, vm => vm.Title); @@ -33,7 +30,6 @@ namespace PettingZoo.UI.Tab { var viewModel = new PublisherViewModel(tabHost, this, connection, fromReceivedMessage); return new ViewTab( - closeTabCommand, new PublisherView(viewModel), viewModel, vm => vm.Title); From 4c6089a9914e1616fc725f094175a717663308ad Mon Sep 17 00:00:00 2001 From: Mark van Renswoude Date: Sat, 18 Dec 2021 14:13:35 +0100 Subject: [PATCH 11/16] Added button for Open in new Publisher tab functionality Removed dropdown arrow for all toolbars --- PettingZoo/UI/Main/MainWindow.xaml | 2 +- PettingZoo/UI/Main/MainWindow.xaml.cs | 12 ++++++++++++ .../UI/Tab/Subscriber/SubscriberView.xaml | 17 ++++++++++++++--- .../UI/Tab/Subscriber/SubscriberView.xaml.cs | 14 ++++++++++++++ .../UI/Tab/Undocked/UndockedTabHostWindow.xaml | 2 +- .../Tab/Undocked/UndockedTabHostWindow.xaml.cs | 13 +++++++++++++ 6 files changed, 55 insertions(+), 5 deletions(-) diff --git a/PettingZoo/UI/Main/MainWindow.xaml b/PettingZoo/UI/Main/MainWindow.xaml index b0fa1a7..6a65b2e 100644 --- a/PettingZoo/UI/Main/MainWindow.xaml +++ b/PettingZoo/UI/Main/MainWindow.xaml @@ -24,7 +24,7 @@ - + + + + - - + + diff --git a/PettingZoo/UI/Tab/Publisher/TapetiPublisherView.xaml b/PettingZoo/UI/Tab/Publisher/TapetiPublisherView.xaml index bda2de9..836dbee 100644 --- a/PettingZoo/UI/Tab/Publisher/TapetiPublisherView.xaml +++ b/PettingZoo/UI/Tab/Publisher/TapetiPublisherView.xaml @@ -48,6 +48,6 @@