Implemented exporting received messages to Tapeti.Cmd compatible JSON file

This commit is contained in:
Mark van Renswoude 2022-01-10 13:41:21 +01:00
parent 785ddbd5b2
commit 2505dad190
25 changed files with 616 additions and 50 deletions

View File

@ -0,0 +1,16 @@
using System.Collections.Generic;
namespace PettingZoo.Core.Export
{
public class ExportFormatProvider : IExportFormatProvider
{
private readonly List<IExportFormat> formats;
public IEnumerable<IExportFormat> Formats => formats;
public ExportFormatProvider(params IExportFormat[] formats)
{
this.formats = new List<IExportFormat>(formats);
}
}
}

View File

@ -0,0 +1,14 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using PettingZoo.Core.Connection;
namespace PettingZoo.Core.Export
{
public interface IExportFormat
{
public string Filter { get; }
public Task Export(Stream stream, IEnumerable<ReceivedMessageInfo> messages);
}
}

View File

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace PettingZoo.Core.Export
{
public interface IExportFormatProvider
{
public IEnumerable<IExportFormat> Formats { get; }
}
}

View File

@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PettingZoo.Core.Connection;
using PettingZoo.Core.Export;
namespace PettingZoo.Tapeti.Export
{
public class TapetiCmdExportFormat : IExportFormat
{
public string Filter => TapetiCmdExportStrings.TapetiCmdFilter;
private static readonly JsonSerializerSettings SerializerSettings = new()
{
NullValueHandling = NullValueHandling.Ignore
};
public async Task Export(Stream stream, IEnumerable<ReceivedMessageInfo> messages)
{
await using var exportFile = new StreamWriter(stream, Encoding.UTF8);
foreach (var message in messages)
{
var serializableMessage = new SerializableMessage
{
Exchange = message.Exchange,
RoutingKey = message.RoutingKey,
Properties = new SerializableMessageProperties
{
AppId = message.Properties.AppId,
ContentEncoding = message.Properties.ContentEncoding,
ContentType = message.Properties.ContentType,
CorrelationId = message.Properties.CorrelationId,
DeliveryMode = message.Properties.DeliveryMode switch
{
MessageDeliveryMode.Persistent => 2,
_ => 1
},
Expiration = message.Properties.Expiration,
Headers = message.Properties.Headers.Count > 0 ? message.Properties.Headers.ToDictionary(p => p.Key, p => p.Value) : null,
MessageId = message.Properties.MessageId,
Priority = message.Properties.Priority,
ReplyTo = message.Properties.ReplyTo,
Timestamp = message.Properties.Timestamp.HasValue ? new DateTimeOffset(message.Properties.Timestamp.Value).ToUnixTimeSeconds() : null,
Type = message.Properties.Type,
UserId = message.Properties.UserId
}
};
var useRawBody = true;
if (message.Properties.ContentType == @"application/json")
{
try
{
if (JToken.Parse(Encoding.UTF8.GetString(message.Body)) is JObject jsonBody)
{
serializableMessage.Body = jsonBody;
useRawBody = false;
}
}
catch
{
// Use raw body
}
}
if (useRawBody)
serializableMessage.RawBody = message.Body;
var serialized = JsonConvert.SerializeObject(serializableMessage, SerializerSettings);
await exportFile.WriteLineAsync(serialized);
}
}
}
// It would be nicer if Tapeti.Cmd exposed it's file format in a NuGet package... if only I knew the author ¯\_(ツ)_/¯
public class SerializableMessage
{
//public ulong DeliveryTag;
//public bool Redelivered;
public string? Exchange;
public string? RoutingKey;
//public string? Queue;
// ReSharper disable once FieldCanBeMadeReadOnly.Local - must be settable by JSON deserialization
public SerializableMessageProperties? Properties;
public JObject? Body;
public byte[]? RawBody;
}
public class SerializableMessageProperties
{
public string? AppId;
//public string? ClusterId;
public string? ContentEncoding;
public string? ContentType;
public string? CorrelationId;
public byte? DeliveryMode;
public string? Expiration;
public IDictionary<string, string>? Headers;
public string? MessageId;
public byte? Priority;
public string? ReplyTo;
public long? Timestamp;
public string? Type;
public string? UserId;
}
}

View File

@ -0,0 +1,72 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
namespace PettingZoo.Tapeti.Export {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// 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 TapetiCmdExportStrings {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal TapetiCmdExportStrings() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[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.Tapeti.Export.TapetiCmdExportStrings", typeof(TapetiCmdExportStrings).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Tapeti.Cmd single-file JSON (*.json)|*.json.
/// </summary>
internal static string TapetiCmdFilter {
get {
return ResourceManager.GetString("TapetiCmdFilter", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="TapetiCmdFilter" xml:space="preserve">
<value>Tapeti.Cmd single-file JSON (*.json)|*.json</value>
</data>
</root>

View File

@ -32,6 +32,11 @@
<AutoGen>True</AutoGen>
<DependentUpon>AssemblyParserStrings.resx</DependentUpon>
</Compile>
<Compile Update="Export\TapetiCmdExportStrings.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>TapetiCmdExportStrings.resx</DependentUpon>
</Compile>
<Compile Update="UI\ClassSelection\ClassSelectionStrings.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
@ -57,6 +62,10 @@
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>AssemblyParserStrings.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Export\TapetiCmdExportStrings.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>TapetiCmdExportStrings.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="UI\ClassSelection\ClassSelectionStrings.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>ClassSelectionStrings.Designer.cs</LastGenOutput>

View File

@ -34,8 +34,6 @@ namespace PettingZoo.Tapeti
.WithSourcesFrom(Path.Combine(PettingZooPaths.InstallationRoot, @"nuget.config"))
.WithSourcesFrom(Path.Combine(PettingZooPaths.AppDataRoot, @"nuget.config"));
var dispatcher = Dispatcher.CurrentDispatcher;
var viewModel = new PackageSelectionViewModel(packageManager);
var selectionWindow = new PackageSelectionWindow(viewModel)
{
@ -44,7 +42,7 @@ namespace PettingZoo.Tapeti
viewModel.Select += (_, args) =>
{
dispatcher.Invoke(() =>
Application.Current.Dispatcher.Invoke(() =>
{
var windowBounds = selectionWindow.RestoreBounds;
selectionWindow.Close();
@ -65,7 +63,7 @@ namespace PettingZoo.Tapeti
// var classes =
var examples = LoadExamples(assemblies);
dispatcher.Invoke(() =>
Application.Current.Dispatcher.Invoke(() =>
{
progressWindow.Close();
progressWindow = null;
@ -90,7 +88,7 @@ namespace PettingZoo.Tapeti
}
catch (Exception e)
{
dispatcher.Invoke(() =>
Application.Current.Dispatcher.Invoke(() =>
{
// ReSharper disable once ConstantConditionalAccessQualifier - if I remove it, there's a "Dereference of a possibly null reference" warning instead
progressWindow?.Close();

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 58 58" style="enable-background:new 0 0 58 58;" xml:space="preserve">
<g>
<g>
<polygon style="fill:#EFEBDE;" points="46.5,14 32.5,0 1.5,0 1.5,58 46.5,58 "/>
<g>
<path style="fill:#D5D0BB;" d="M11.5,23h25c0.552,0,1-0.447,1-1s-0.448-1-1-1h-25c-0.552,0-1,0.447-1,1S10.948,23,11.5,23z"/>
<path style="fill:#D5D0BB;" d="M11.5,15h10c0.552,0,1-0.447,1-1s-0.448-1-1-1h-10c-0.552,0-1,0.447-1,1S10.948,15,11.5,15z"/>
<path style="fill:#D5D0BB;" d="M36.5,29h-25c-0.552,0-1,0.447-1,1s0.448,1,1,1h25c0.552,0,1-0.447,1-1S37.052,29,36.5,29z"/>
<path style="fill:#D5D0BB;" d="M36.5,37h-25c-0.552,0-1,0.447-1,1s0.448,1,1,1h25c0.552,0,1-0.447,1-1S37.052,37,36.5,37z"/>
<path style="fill:#D5D0BB;" d="M36.5,45h-25c-0.552,0-1,0.447-1,1s0.448,1,1,1h25c0.552,0,1-0.447,1-1S37.052,45,36.5,45z"/>
</g>
<polygon style="fill:#D5D0BB;" points="32.5,0 32.5,14 46.5,14 "/>
</g>
<g>
<rect x="34.5" y="36" style="fill:#21AE5E;" width="22" height="22"/>
<rect x="44.5" y="37.586" style="fill:#FFFFFF;" width="2" height="16"/>
<polygon style="fill:#FFFFFF;" points="45.5,55 38.5,48.293 39.976,46.879 45.5,52.172 51.024,46.879 52.5,48.293 "/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 58 58" style="enable-background:new 0 0 58 58;" xml:space="preserve">
<g>
<g>
<polygon style="fill:#EFEBDE;" points="46,14 32,0 1,0 1,58 46,58 "/>
<g>
<path style="fill:#D5D0BB;" d="M11,23h25c0.552,0,1-0.447,1-1s-0.448-1-1-1H11c-0.552,0-1,0.447-1,1S10.448,23,11,23z"/>
<path style="fill:#D5D0BB;" d="M11,15h10c0.552,0,1-0.447,1-1s-0.448-1-1-1H11c-0.552,0-1,0.447-1,1S10.448,15,11,15z"/>
<path style="fill:#D5D0BB;" d="M36,29H11c-0.552,0-1,0.447-1,1s0.448,1,1,1h25c0.552,0,1-0.447,1-1S36.552,29,36,29z"/>
<path style="fill:#D5D0BB;" d="M36,37H11c-0.552,0-1,0.447-1,1s0.448,1,1,1h25c0.552,0,1-0.447,1-1S36.552,37,36,37z"/>
<path style="fill:#D5D0BB;" d="M36,45H11c-0.552,0-1,0.447-1,1s0.448,1,1,1h25c0.552,0,1-0.447,1-1S36.552,45,36,45z"/>
</g>
<polygon style="fill:#D5D0BB;" points="32,0 32,14 46,14 "/>
</g>
<g>
<rect x="35" y="36" style="fill:#48A0DC;" width="22" height="22"/>
<rect x="45" y="42" style="fill:#FFFFFF;" width="2" height="16"/>
<polygon style="fill:#FFFFFF;" points="51.293,48.707 46,43.414 40.707,48.707 39.293,47.293 46,40.586 52.707,47.293 "/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -23,7 +23,9 @@
<None Remove="Images\Dock.svg" />
<None Remove="Images\Error.svg" />
<None Remove="Images\Example.svg" />
<None Remove="Images\Export.svg" />
<None Remove="Images\Folder.svg" />
<None Remove="Images\Import.svg" />
<None Remove="Images\Ok.svg" />
<None Remove="Images\PublishSend.svg" />
<None Remove="Images\Undock.svg" />
@ -36,7 +38,9 @@
<Resource Include="Images\Dock.svg" />
<Resource Include="Images\Error.svg" />
<Resource Include="Images\Example.svg" />
<Resource Include="Images\Export.svg" />
<Resource Include="Images\Folder.svg" />
<Resource Include="Images\Import.svg" />
<Resource Include="Images\Ok.svg" />
<Resource Include="Images\Publish.svg" />
<Resource Include="Images\PublishSend.svg" />

View File

@ -4,14 +4,17 @@ using System.IO;
using System.Windows;
using System.Windows.Markup;
using PettingZoo.Core.Connection;
using PettingZoo.Core.Export;
using PettingZoo.Core.Generator;
using PettingZoo.Core.Settings;
using PettingZoo.RabbitMQ;
using PettingZoo.Settings.LiteDB;
using PettingZoo.Tapeti;
using PettingZoo.Tapeti.Export;
using PettingZoo.UI.Connection;
using PettingZoo.UI.Main;
using PettingZoo.UI.Subscribe;
using PettingZoo.UI.Tab;
using Serilog;
using SimpleInjector;
@ -79,6 +82,10 @@ namespace PettingZoo
container.Register<IConnectionSettingsRepository, LiteDBConnectionSettingsRepository>();
container.Register<IUISettingsRepository, LiteDBUISettingsRepository>();
container.Register<IExampleGenerator, TapetiClassLibraryExampleGenerator>();
container.Register<ITabHostProvider, TabHostProvider>();
container.Register<ITabFactory, ViewTabFactory>();
container.RegisterInstance<IExportFormatProvider>(new ExportFormatProvider(new TapetiCmdExportFormat()));
container.Register<MainWindow>();

View File

@ -5,7 +5,6 @@
Should-have
-----------
- Save / load publisher messages (either as templates or to disk)
- Tapeti: export received messages to Tapeti.Cmd JSON file / Tapeti.Cmd command-line
- Tapeti: import Tapeti.Cmd JSON file into Subscriber-esque tab for easier browsing
- Tapeti: fetch NuGet dependencies to improve the chances of succesfully loading the assembly, instead of the current "extraAssembliesPaths" workaround

View File

@ -5,7 +5,6 @@ using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using PettingZoo.Core.Connection;
using PettingZoo.Core.Generator;
using PettingZoo.UI.Connection;
using PettingZoo.UI.Subscribe;
using PettingZoo.UI.Tab;
@ -20,11 +19,12 @@ namespace PettingZoo.UI.Main
public bool WasMaximized;
public MainWindow(IConnectionFactory connectionFactory, IConnectionDialog connectionDialog, ISubscribeDialog subscribeDialog, IExampleGenerator exampleGenerator)
public MainWindow(IConnectionFactory connectionFactory, IConnectionDialog connectionDialog, ISubscribeDialog subscribeDialog,
ITabHostProvider tabHostProvider, ITabFactory tabFactory)
{
WindowStartupLocation = WindowStartupLocation.CenterScreen;
viewModel = new MainWindowViewModel(connectionFactory, connectionDialog, subscribeDialog, this, exampleGenerator)
viewModel = new MainWindowViewModel(connectionFactory, connectionDialog, subscribeDialog, this, tabHostProvider, tabFactory)
{
TabHostWindow = this
};

View File

@ -6,7 +6,6 @@ using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using PettingZoo.Core.Connection;
using PettingZoo.Core.Generator;
using PettingZoo.UI.Connection;
using PettingZoo.UI.Subscribe;
using PettingZoo.UI.Tab;
@ -29,6 +28,7 @@ namespace PettingZoo.UI.Main
private readonly IConnectionDialog connectionDialog;
private readonly ISubscribeDialog subscribeDialog;
private readonly ITabContainer tabContainer;
private readonly ITabHostProvider tabHostProvider;
private readonly ITabFactory tabFactory;
private SubscribeDialogParams? subscribeDialogParams;
@ -103,12 +103,16 @@ namespace PettingZoo.UI.Main
public MainWindowViewModel(IConnectionFactory connectionFactory, IConnectionDialog connectionDialog,
ISubscribeDialog subscribeDialog, ITabContainer tabContainer, IExampleGenerator exampleGenerator)
ISubscribeDialog subscribeDialog, ITabContainer tabContainer, ITabHostProvider tabHostProvider, ITabFactory tabFactory)
{
tabHostProvider.SetInstance(this);
this.connectionFactory = connectionFactory;
this.connectionDialog = connectionDialog;
this.subscribeDialog = subscribeDialog;
this.tabContainer = tabContainer;
this.tabHostProvider = tabHostProvider;
this.tabFactory = tabFactory;
connectionStatus = GetConnectionStatus(null);
connectionStatusType = ConnectionStatusType.Error;
@ -120,8 +124,6 @@ namespace PettingZoo.UI.Main
subscribeCommand = new DelegateCommand(SubscribeExecute, IsConnectedCanExecute);
closeTabCommand = new DelegateCommand(CloseTabExecute, HasActiveTabCanExecute);
undockTabCommand = new DelegateCommand(UndockTabExecute, HasActiveTabCanExecute);
tabFactory = new ViewTabFactory(this, exampleGenerator);
}
@ -226,7 +228,7 @@ namespace PettingZoo.UI.Main
if (tab == null)
return;
var tabHostWindow = UndockedTabHostWindow.Create(this, tab, tabContainer.TabWidth, tabContainer.TabHeight);
var tabHostWindow = UndockedTabHostWindow.Create(tabHostProvider, tab, tabContainer.TabWidth, tabContainer.TabHeight);
undockedTabs.Add(tab, tabHostWindow);
tabHostWindow.Show();
@ -330,7 +332,7 @@ namespace PettingZoo.UI.Main
public class DesignTimeMainWindowViewModel : MainWindowViewModel
{
public DesignTimeMainWindowViewModel() : base(null!, null!, null!, null!, null!)
public DesignTimeMainWindowViewModel() : base(null!, null!, null!, null!, null!, null!)
{
}
}

View File

@ -0,0 +1,9 @@
namespace PettingZoo.UI.Tab
{
public interface ITabHostProvider
{
public ITabHost Instance { get; }
public void SetInstance(ITabHost instance);
}
}

View File

@ -21,7 +21,7 @@ namespace PettingZoo.UI.Tab.Publisher
private readonly IConnection connection;
private readonly IExampleGenerator exampleGenerator;
private readonly ITabFactory tabFactory;
private readonly ITabHost tabHost;
private readonly ITabHostProvider tabHostProvider;
private bool sendToExchange = true;
private string exchange = "";
@ -156,12 +156,12 @@ namespace PettingZoo.UI.Tab.Publisher
string IPublishDestination.RoutingKey => SendToExchange ? RoutingKey : Queue;
public PublisherViewModel(ITabHost tabHost, ITabFactory tabFactory, IConnection connection, IExampleGenerator exampleGenerator, ReceivedMessageInfo? fromReceivedMessage = null)
public PublisherViewModel(ITabHostProvider tabHostProvider, ITabFactory tabFactory, IConnection connection, IExampleGenerator exampleGenerator, ReceivedMessageInfo? fromReceivedMessage = null)
{
this.connection = connection;
this.exampleGenerator = exampleGenerator;
this.tabFactory = tabFactory;
this.tabHost = tabHost;
this.tabHostProvider = tabHostProvider;
publishCommand = new DelegateCommand(PublishExecute, PublishCanExecute);
@ -287,7 +287,7 @@ namespace PettingZoo.UI.Tab.Publisher
var subscriber = connection.Subscribe();
var tab = tabFactory.CreateSubscriberTab(connection, subscriber);
tabHost.AddTab(tab);
tabHostProvider.Instance.AddTab(tab);
subscriber.Start();
return subscriber.QueueName;

View File

@ -124,7 +124,7 @@ namespace PettingZoo.UI.Tab.Publisher
{
exampleGenerator.Select(tabHostWindow, example =>
{
Dispatcher.CurrentDispatcher.BeginInvoke(() =>
Application.Current.Dispatcher.BeginInvoke(() =>
{
switch (example)
{

View File

@ -1,26 +1,33 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using Microsoft.Win32;
using PettingZoo.Core.Connection;
using PettingZoo.Core.Export;
using PettingZoo.Core.Rendering;
using PettingZoo.WPF.ViewModel;
using Serilog;
using IConnection = PettingZoo.Core.Connection.IConnection;
namespace PettingZoo.UI.Tab.Subscriber
{
public class SubscriberViewModel : BaseViewModel, ITabToolbarCommands, ITabActivate
{
private readonly ITabHost tabHost;
private readonly ILogger logger;
private readonly ITabHostProvider tabHostProvider;
private readonly ITabFactory tabFactory;
private readonly IConnection connection;
private readonly ISubscriber subscriber;
private readonly Dispatcher dispatcher;
private readonly IExportFormatProvider exportFormatProvider;
private ReceivedMessageInfo? selectedMessage;
private readonly DelegateCommand clearCommand;
private readonly DelegateCommand exportCommand;
private readonly TabToolbarCommand[] toolbarCommands;
private IDictionary<string, string>? selectedMessageProperties;
@ -32,6 +39,7 @@ namespace PettingZoo.UI.Tab.Subscriber
public ICommand ClearCommand => clearCommand;
public ICommand ExportCommand => exportCommand;
// ReSharper disable once UnusedMember.Global - it is, but via a proxy
public ICommand CreatePublisherCommand => createPublisherCommand;
@ -70,22 +78,25 @@ namespace PettingZoo.UI.Tab.Subscriber
public IEnumerable<TabToolbarCommand> ToolbarCommands => toolbarCommands;
public SubscriberViewModel(ITabHost tabHost, ITabFactory tabFactory, IConnection connection, ISubscriber subscriber)
public SubscriberViewModel(ILogger logger, ITabHostProvider tabHostProvider, ITabFactory tabFactory, IConnection connection, ISubscriber subscriber, IExportFormatProvider exportFormatProvider)
{
this.tabHost = tabHost;
this.logger = logger;
this.tabHostProvider = tabHostProvider;
this.tabFactory = tabFactory;
this.connection = connection;
this.subscriber = subscriber;
dispatcher = Dispatcher.CurrentDispatcher;
this.exportFormatProvider = exportFormatProvider;
Messages = new ObservableCollectionEx<ReceivedMessageInfo>();
UnreadMessages = new ObservableCollectionEx<ReceivedMessageInfo>();
clearCommand = new DelegateCommand(ClearExecute, ClearCanExecute);
clearCommand = new DelegateCommand(ClearExecute, HasMessagesCanExecute);
exportCommand = new DelegateCommand(ExportExecute, HasMessagesCanExecute);
toolbarCommands = new[]
{
new TabToolbarCommand(ClearCommand, SubscriberViewStrings.CommandClear, SvgIconHelper.LoadFromResource("/Images/Clear.svg"))
new TabToolbarCommand(ClearCommand, SubscriberViewStrings.CommandClear, SvgIconHelper.LoadFromResource("/Images/Clear.svg")),
new TabToolbarCommand(ExportCommand, SubscriberViewStrings.CommandExport, SvgIconHelper.LoadFromResource("/Images/Export.svg"))
};
createPublisherCommand = new DelegateCommand(CreatePublisherExecute, CreatePublisherCanExecute);
@ -94,26 +105,82 @@ namespace PettingZoo.UI.Tab.Subscriber
subscriber.Start();
}
private void ClearExecute()
{
Messages.Clear();
UnreadMessages.Clear();
HasMessagesChanged();
RaisePropertyChanged(nameof(UnreadMessagesVisibility));
clearCommand.RaiseCanExecuteChanged();
}
private bool ClearCanExecute()
private bool HasMessagesCanExecute()
{
return Messages.Count > 0;
return Messages.Count > 0 || UnreadMessages.Count > 0;
}
private void HasMessagesChanged()
{
clearCommand.RaiseCanExecuteChanged();
exportCommand.RaiseCanExecuteChanged();
}
private void ExportExecute()
{
var formats = exportFormatProvider.Formats.ToArray();
var dialog = new SaveFileDialog
{
Filter = string.Join('|', formats.Select(f => f.Filter))
};
if (!dialog.ShowDialog().GetValueOrDefault())
return;
// 1-based? Seriously?
if (dialog.FilterIndex <= 0 || dialog.FilterIndex > formats.Length)
return;
var messages = Messages.Concat(UnreadMessages).ToArray();
var filename = dialog.FileName;
var format = formats[dialog.FilterIndex - 1];
Task.Run(async () =>
{
try
{
await using var exportFile = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.Read);
await format.Export(exportFile, messages);
await Application.Current.Dispatcher.BeginInvoke(() =>
{
MessageBox.Show(string.Format(SubscriberViewStrings.ExportSuccess, messages.Length, filename),
SubscriberViewStrings.ExportResultTitle,
MessageBoxButton.OK, MessageBoxImage.Information);
});
}
catch (Exception e)
{
logger.Error(e, "Error while exporting messages");
await Application.Current.Dispatcher.BeginInvoke(() =>
{
MessageBox.Show(string.Format(SubscriberViewStrings.ExportError, e.Message),
SubscriberViewStrings.ExportResultTitle,
MessageBoxButton.OK, MessageBoxImage.Information);
});
}
});
}
private void CreatePublisherExecute()
{
var publisherTab = tabFactory.CreatePublisherTab(connection, SelectedMessage);
tabHost.AddTab(publisherTab);
tabHostProvider.Instance.AddTab(publisherTab);
}
@ -125,7 +192,7 @@ namespace PettingZoo.UI.Tab.Subscriber
private void SubscriberMessageReceived(object? sender, MessageReceivedEventArgs args)
{
dispatcher.BeginInvoke(() =>
Application.Current.Dispatcher.BeginInvoke(() =>
{
if (!tabActive)
{
@ -139,7 +206,7 @@ namespace PettingZoo.UI.Tab.Subscriber
else
Messages.Add(args.MessageInfo);
clearCommand.RaiseCanExecuteChanged();
HasMessagesChanged();
});
}
@ -168,7 +235,7 @@ namespace PettingZoo.UI.Tab.Subscriber
newMessageTimer = new Timer(
_ =>
{
dispatcher.BeginInvoke(() =>
Application.Current.Dispatcher.BeginInvoke(() =>
{
if (UnreadMessages.Count == 0)
return;
@ -210,7 +277,7 @@ namespace PettingZoo.UI.Tab.Subscriber
public class DesignTimeSubscriberViewModel : SubscriberViewModel
{
public DesignTimeSubscriberViewModel() : base(null!, null!, null!, new DesignTimeSubscriber())
public DesignTimeSubscriberViewModel() : base(null!, null!, null!, null!, new DesignTimeSubscriber(), null!)
{
for (var i = 1; i <= 5; i++)
(i > 2 ? UnreadMessages : Messages).Add(new ReceivedMessageInfo(

View File

@ -69,6 +69,15 @@ namespace PettingZoo.UI.Tab.Subscriber {
}
}
/// <summary>
/// Looks up a localized string similar to Export....
/// </summary>
public static string CommandExport {
get {
return ResourceManager.GetString("CommandExport", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open in new Publisher tab.
/// </summary>
@ -96,6 +105,33 @@ namespace PettingZoo.UI.Tab.Subscriber {
}
}
/// <summary>
/// Looks up a localized string similar to Error while exporting messages: {0}.
/// </summary>
public static string ExportError {
get {
return ResourceManager.GetString("ExportError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Export.
/// </summary>
public static string ExportResultTitle {
get {
return ResourceManager.GetString("ExportResultTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Exported {0} message(s) to {1}.
/// </summary>
public static string ExportSuccess {
get {
return ResourceManager.GetString("ExportSuccess", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to New messages.
/// </summary>

View File

@ -120,6 +120,9 @@
<data name="CommandClear" xml:space="preserve">
<value>Clear</value>
</data>
<data name="CommandExport" xml:space="preserve">
<value>Export...</value>
</data>
<data name="ContextPublish" xml:space="preserve">
<value>Open in new Publisher tab</value>
</data>
@ -129,6 +132,15 @@
<data name="DeliveryModePersistent" xml:space="preserve">
<value>Persistent</value>
</data>
<data name="ExportError" xml:space="preserve">
<value>Error while exporting messages: {0}</value>
</data>
<data name="ExportResultTitle" xml:space="preserve">
<value>Export</value>
</data>
<data name="ExportSuccess" xml:space="preserve">
<value>Exported {0} message(s) to {1}</value>
</data>
<data name="LabelNewMessages" xml:space="preserve">
<value>New messages</value>
</data>

View File

@ -0,0 +1,18 @@
using System;
namespace PettingZoo.UI.Tab
{
public class TabHostProvider : ITabHostProvider
{
private ITabHost? instance;
public ITabHost Instance => instance ?? throw new InvalidOperationException("ITabHost instance must be initialized before acquiring");
// ReSharper disable once ParameterHidesMember
public void SetInstance(ITabHost instance)
{
this.instance = instance;
}
}
}

View File

@ -10,7 +10,7 @@ namespace PettingZoo.UI.Tab.Undocked
{
public class UndockedTabHostViewModel : BaseViewModel, ITabActivate
{
private readonly ITabHost tabHost;
private readonly ITabHostProvider tabHostProvider;
private readonly ITab tab;
private readonly DelegateCommand dockCommand;
@ -25,9 +25,9 @@ namespace PettingZoo.UI.Tab.Undocked
public ICommand DockCommand => dockCommand;
public UndockedTabHostViewModel(ITabHost tabHost, ITab tab)
public UndockedTabHostViewModel(ITabHostProvider tabHostProvider, ITab tab)
{
this.tabHost = tabHost;
this.tabHostProvider = tabHostProvider;
this.tab = tab;
tab.PropertyChanged += (_, args) =>
@ -43,13 +43,13 @@ namespace PettingZoo.UI.Tab.Undocked
private void DockCommandExecute()
{
tabHost.DockTab(tab);
tabHostProvider.Instance.DockTab(tab);
}
public void WindowClosed()
{
tabHost.UndockedTabClosed(tab);
tabHostProvider.Instance.UndockedTabClosed(tab);
}

View File

@ -10,9 +10,9 @@ namespace PettingZoo.UI.Tab.Undocked
/// </summary>
public partial class UndockedTabHostWindow
{
public static UndockedTabHostWindow Create(ITabHost tabHost, ITab tab, double width, double height)
public static UndockedTabHostWindow Create(ITabHostProvider tabHostProvider, ITab tab, double width, double height)
{
var viewModel = new UndockedTabHostViewModel(tabHost, tab);
var viewModel = new UndockedTabHostViewModel(tabHostProvider, tab);
var window = new UndockedTabHostWindow(viewModel)
{
Width = width,

View File

@ -1,26 +1,32 @@
using PettingZoo.Core.Connection;
using PettingZoo.Core.Export;
using PettingZoo.Core.Generator;
using PettingZoo.UI.Tab.Publisher;
using PettingZoo.UI.Tab.Subscriber;
using Serilog;
namespace PettingZoo.UI.Tab
{
public class ViewTabFactory : ITabFactory
{
private readonly ITabHost tabHost;
private readonly ILogger logger;
private readonly ITabHostProvider tabHostProvider;
private readonly IExampleGenerator exampleGenerator;
private readonly IExportFormatProvider exportFormatProvider;
public ViewTabFactory(ITabHost tabHost, IExampleGenerator exampleGenerator)
public ViewTabFactory(ILogger logger, ITabHostProvider tabHostProvider, IExampleGenerator exampleGenerator, IExportFormatProvider exportFormatProvider)
{
this.tabHost = tabHost;
this.logger = logger;
this.tabHostProvider = tabHostProvider;
this.exampleGenerator = exampleGenerator;
this.exportFormatProvider = exportFormatProvider;
}
public ITab CreateSubscriberTab(IConnection connection, ISubscriber subscriber)
{
var viewModel = new SubscriberViewModel(tabHost, this, connection, subscriber);
var viewModel = new SubscriberViewModel(logger, tabHostProvider, this, connection, subscriber, exportFormatProvider);
return new ViewTab<SubscriberView, SubscriberViewModel>(
new SubscriberView(viewModel),
viewModel,
@ -30,7 +36,7 @@ namespace PettingZoo.UI.Tab
public ITab CreatePublisherTab(IConnection connection, ReceivedMessageInfo? fromReceivedMessage = null)
{
var viewModel = new PublisherViewModel(tabHost, this, connection, exampleGenerator, fromReceivedMessage);
var viewModel = new PublisherViewModel(tabHostProvider, this, connection, exampleGenerator, fromReceivedMessage);
return new ViewTab<PublisherView, PublisherViewModel>(
new PublisherView(viewModel),
viewModel,