1
0
mirror of synced 2024-11-23 04:53:51 +00:00

Implemented raw publishing

This commit is contained in:
Mark van Renswoude 2021-12-06 14:08:29 +01:00
parent 503507422d
commit 229fc9415d
28 changed files with 1786 additions and 193 deletions

View File

@ -8,7 +8,7 @@ namespace PettingZoo.Core.Connection
event EventHandler<StatusChangedEventArgs> StatusChanged;
ISubscriber Subscribe(string exchange, string routingKey);
Task Publish(MessageInfo messageInfo);
Task Publish(PublishMessageInfo messageInfo);
}

View File

@ -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;
}

View File

@ -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<string, string> Properties { get; }
public MessageProperties Properties { get; }
public MessageInfo(string exchange, string routingKey, byte[] body, IDictionary<string, string> 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<string, string> EmptyHeaders = new Dictionary<string, string>();
public MessageProperties(IReadOnlyDictionary<string, string>? 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<string, string> 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; }
}
}

View File

@ -9,4 +9,19 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<ItemGroup>
<Compile Update="Rendering\MessagePropertiesRendererStrings.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>MessagePropertiesRendererStrings.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Rendering\MessagePropertiesRendererStrings.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>MessagePropertiesRendererStrings.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@ -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);

View File

@ -0,0 +1,60 @@
using System.Collections.Generic;
using PettingZoo.Core.Connection;
namespace PettingZoo.Core.Rendering
{
public class MessagePropertiesRenderer
{
public static IDictionary<string, string> Render(MessageProperties properties)
{
var result = new Dictionary<string, string>();
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;
}
}
}

View File

@ -0,0 +1,198 @@
//------------------------------------------------------------------------------
// <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.Core.Rendering {
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 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() {
}
/// <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.Core.Rendering.MessagePropertiesRendererStrings", typeof(MessagePropertiesRendererStrings).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 App ID.
/// </summary>
internal static string AppId {
get {
return ResourceManager.GetString("AppId", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Content encoding.
/// </summary>
internal static string ContentEncoding {
get {
return ResourceManager.GetString("ContentEncoding", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Content type.
/// </summary>
internal static string ContentType {
get {
return ResourceManager.GetString("ContentType", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Correlation ID.
/// </summary>
internal static string CorrelationId {
get {
return ResourceManager.GetString("CorrelationId", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Delivery mode.
/// </summary>
internal static string DeliveryMode {
get {
return ResourceManager.GetString("DeliveryMode", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Transient (1).
/// </summary>
internal static string DeliveryModeNonPersistent {
get {
return ResourceManager.GetString("DeliveryModeNonPersistent", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Persistent (2).
/// </summary>
internal static string DeliveryModePersistent {
get {
return ResourceManager.GetString("DeliveryModePersistent", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Expiration.
/// </summary>
internal static string Expiration {
get {
return ResourceManager.GetString("Expiration", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Header: .
/// </summary>
internal static string HeaderPrefix {
get {
return ResourceManager.GetString("HeaderPrefix", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Message ID.
/// </summary>
internal static string MessageId {
get {
return ResourceManager.GetString("MessageId", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Priority.
/// </summary>
internal static string Priority {
get {
return ResourceManager.GetString("Priority", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Reply To.
/// </summary>
internal static string ReplyTo {
get {
return ResourceManager.GetString("ReplyTo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Timestamp.
/// </summary>
internal static string Timestamp {
get {
return ResourceManager.GetString("Timestamp", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Type.
/// </summary>
internal static string Type {
get {
return ResourceManager.GetString("Type", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to User ID.
/// </summary>
internal static string UserId {
get {
return ResourceManager.GetString("UserId", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,165 @@
<?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=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="AppId" xml:space="preserve">
<value>App ID</value>
</data>
<data name="ContentEncoding" xml:space="preserve">
<value>Content encoding</value>
</data>
<data name="ContentType" xml:space="preserve">
<value>Content type</value>
</data>
<data name="CorrelationId" xml:space="preserve">
<value>Correlation ID</value>
</data>
<data name="DeliveryMode" xml:space="preserve">
<value>Delivery mode</value>
</data>
<data name="DeliveryModeNonPersistent" xml:space="preserve">
<value>Transient (1)</value>
</data>
<data name="DeliveryModePersistent" xml:space="preserve">
<value>Persistent (2)</value>
</data>
<data name="Expiration" xml:space="preserve">
<value>Expiration</value>
</data>
<data name="HeaderPrefix" xml:space="preserve">
<value>Header: </value>
</data>
<data name="MessageId" xml:space="preserve">
<value>Message ID</value>
</data>
<data name="Priority" xml:space="preserve">
<value>Priority</value>
</data>
<data name="ReplyTo" xml:space="preserve">
<value>Reply To</value>
</data>
<data name="Timestamp" xml:space="preserve">
<value>Timestamp</value>
</data>
<data name="Type" xml:space="preserve">
<value>Type</value>
</data>
<data name="UserId" xml:space="preserve">
<value>User ID</value>
</data>
</root>

View File

@ -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");

View File

@ -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<string, string> Convert(IBasicProperties basicProperties)
public static MessageProperties Convert(IBasicProperties basicProperties)
{
var properties = new Dictionary<string, string>();
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<string, string> 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<string, object>();
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;
}

View File

@ -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(),

View File

@ -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";
}
}

View File

@ -1,14 +0,0 @@
using System.Collections.Generic;
namespace PettingZoo.RabbitMQ
{
public static class RabbitMQPropertiesExtensions
{
public static string ContentType(this IDictionary<string, string> properties)
{
return properties.TryGetValue(RabbitMQProperties.ContentType, out var value)
? value
: "";
}
}
}

View File

@ -56,6 +56,11 @@
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
<Compile Update="UI\Tab\Publisher\RawPublisherViewStrings.Designer.cs">
<DependentUpon>RawPublisherViewStrings.resx</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
<Compile Update="UI\Tab\Publisher\PublisherViewStrings.Designer.cs">
<DependentUpon>PublisherViewStrings.resx</DependentUpon>
<DesignTime>True</DesignTime>
@ -81,6 +86,10 @@
<LastGenOutput>SubscribeWindowStrings.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="UI\Tab\Publisher\RawPublisherViewStrings.resx">
<LastGenOutput>RawPublisherViewStrings.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="UI\Tab\Publisher\PublisherViewStrings.resx">
<LastGenOutput>PublisherViewStrings.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>

View File

@ -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

View File

@ -10,6 +10,11 @@
<Setter Property="Padding" Value="8,4"/>
</Style>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Padding" Value="3" />
</Style>
<!-- Explicit styling -->
<Style x:Key="SidePanel" TargetType="{x:Type Border}">
<Setter Property="BorderThickness" Value="1"/>
@ -59,8 +64,17 @@
</Style.Triggers>
</Style>
<Style x:Key="RoutingKey">
</Style>
<Style x:Key="TypeSelection" TargetType="{x:Type FrameworkElement}">
<Setter Property="Margin" Value="0 0 8 0" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style x:Key="SectionLabel" TargetType="{x:Type Label}">
<Setter Property="FontWeight" Value="Bold"/>
</Style>
</ResourceDictionary>

View File

@ -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}"

View File

@ -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);
}

View File

@ -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;

View File

@ -9,13 +9,20 @@
d:DesignWidth="800"
d:DataContext="{d:DesignInstance res:DesignTimePublisherViewModel, IsDesignTimeCreatable=True}"
Background="White">
<StackPanel Margin="4">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{x:Static res:PublisherViewStrings.LabelMessageType}" Style="{StaticResource TypeSelection}" />
<Grid Margin="4">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row="0">
<Label Content="{x:Static res:PublisherViewStrings.LabelMessageType}" Style="{StaticResource TypeSelection}" />
<RadioButton Content="{x:Static res:PublisherViewStrings.OptionMessageTypeRaw}" Style="{StaticResource TypeSelection}" IsChecked="{Binding MessageTypeRaw}" />
<RadioButton Content="{x:Static res:PublisherViewStrings.OptionMessageTypeTapeti}" Style="{StaticResource TypeSelection}" IsChecked="{Binding MessageTypeTapeti}" />
</StackPanel>
<TextBlock>TODO: implement publish forms</TextBlock>
</StackPanel>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<ContentControl Margin="0 8 0 0" Content="{Binding MessageTypeControl}" />
</ScrollViewer>
</Grid>
</UserControl>

View File

@ -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();
}
}
}

View File

@ -0,0 +1,166 @@
<UserControl x:Class="PettingZoo.UI.Tab.Publisher.RawPublisherView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:publisher="clr-namespace:PettingZoo.UI.Tab.Publisher"
xmlns:ui="clr-namespace:PettingZoo.UI"
mc:Ignorable="d"
d:DesignHeight="800" d:DesignWidth="800"
d:DataContext="{d:DesignInstance publisher:DesignTimeRawPublisherViewModel, IsDesignTimeCreatable=True}"
Background="White">
<ui:GridLayout Style="{StaticResource Form}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:GridLayout.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="16"/>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="16"/>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</ui:GridLayout.RowDefinitions>
<Label Grid.Row="0" Grid.Column="1">
<StackPanel Orientation="Horizontal">
<RadioButton Content="{x:Static publisher:RawPublisherViewStrings.LabelSendToExchange}" IsChecked="{Binding SendToExchange}" Style="{StaticResource TypeSelection}" />
<RadioButton Content="{x:Static publisher:RawPublisherViewStrings.LabelSendToQueue}" IsChecked="{Binding SendToQueue}" Style="{StaticResource TypeSelection}" />
</StackPanel>
</Label>
<Label Grid.Row="1" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelExchange}" Visibility="{Binding ExchangeVisibility}" />
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Exchange}" Visibility="{Binding ExchangeVisibility}" />
<Label Grid.Row="2" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelRoutingKey}" Visibility="{Binding ExchangeVisibility}" />
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding RoutingKey}" Visibility="{Binding ExchangeVisibility}" />
<Label Grid.Row="3" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelQueue}" Visibility="{Binding QueueVisibility}" />
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding Queue}" Visibility="{Binding QueueVisibility}" />
<Label Grid.Row="5" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelDeliveryMode}" />
<ComboBox Grid.Row="5" Grid.Column="1" SelectedIndex="{Binding DeliveryModeIndex}">
<ComboBoxItem Content="{x:Static publisher:RawPublisherViewStrings.DeliveryModeNonPersistent}" />
<ComboBoxItem Content="{x:Static publisher:RawPublisherViewStrings.DeliveryModePersistent}" />
</ComboBox>
<Label Grid.Row="6" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelHeaders}" />
<ItemsControl Grid.Row="6" Grid.Column="1" ItemsSource="{Binding Headers}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="0 0 0 8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" >
<TextBox Text="{Binding Key, UpdateSourceTrigger=PropertyChanged}" LostFocus="Header_OnLostFocus" Margin="0 0 8 0" x:Name="HeaderKey" />
<TextBlock IsHitTestVisible="False" Text="{x:Static publisher:RawPublisherViewStrings.HeaderName}" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="10,0,0,0" Foreground="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Text, ElementName=HeaderKey}" Value="">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
<Grid Grid.Column="1">
<TextBox Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}" LostFocus="Header_OnLostFocus" x:Name="HeaderValue" />
<TextBlock IsHitTestVisible="False" Text="{x:Static publisher:RawPublisherViewStrings.HeaderValue}" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="10,0,0,0" Foreground="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Text, ElementName=HeaderValue}" Value="">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Label Grid.Row="7" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelProperties}" Style="{StaticResource SectionLabel}"/>
<Label Grid.Row="8" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelContentType}" />
<TextBox Grid.Row="8" Grid.Column="1" Text="{Binding ContentType}" />
<Label Grid.Row="9" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelCorrelationId}" />
<TextBox Grid.Row="9" Grid.Column="1" Text="{Binding CorrelationId}" />
<Label Grid.Row="10" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelReplyTo}" />
<TextBox Grid.Row="10" Grid.Column="1" Text="{Binding ReplyTo}" />
<Label Grid.Row="11" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelAppId}" Visibility="{Binding PropertiesExpandedVisibility}" />
<TextBox Grid.Row="11" Grid.Column="1" Text="{Binding AppId}" Visibility="{Binding PropertiesExpandedVisibility}" />
<Label Grid.Row="12" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelContentEncoding}" Visibility="{Binding PropertiesExpandedVisibility}" />
<TextBox Grid.Row="12" Grid.Column="1" Text="{Binding ContentEncoding}" Visibility="{Binding PropertiesExpandedVisibility}" />
<Label Grid.Row="13" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelExpiration}" Visibility="{Binding PropertiesExpandedVisibility}" />
<TextBox Grid.Row="13" Grid.Column="1" Text="{Binding Expiration}" Visibility="{Binding PropertiesExpandedVisibility}" />
<Label Grid.Row="14" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelMessageId}" Visibility="{Binding PropertiesExpandedVisibility}" />
<TextBox Grid.Row="14" Grid.Column="1" Text="{Binding MessageId}" Visibility="{Binding PropertiesExpandedVisibility}" />
<Label Grid.Row="15" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelPriority}" Visibility="{Binding PropertiesExpandedVisibility}" />
<TextBox Grid.Row="15" Grid.Column="1" Text="{Binding Priority}" Visibility="{Binding PropertiesExpandedVisibility}" />
<Label Grid.Row="16" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelTimestamp}" Visibility="{Binding PropertiesExpandedVisibility}" />
<TextBox Grid.Row="16" Grid.Column="1" Text="{Binding Timestamp}" Visibility="{Binding PropertiesExpandedVisibility}" />
<Label Grid.Row="17" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelType}" Visibility="{Binding PropertiesExpandedVisibility}" />
<TextBox Grid.Row="17" Grid.Column="1" Text="{Binding TypeProperty}" Visibility="{Binding PropertiesExpandedVisibility}" />
<Label Grid.Row="18" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelUserId}" Visibility="{Binding PropertiesExpandedVisibility}" />
<TextBox Grid.Row="18" Grid.Column="1" Text="{Binding UserId}" Visibility="{Binding PropertiesExpandedVisibility}" />
<Button Grid.Row="19" Grid.Column="1" Content="{Binding PropertiesExpandedCollapsedText}" Command="{Binding PropertiesExpandCollapseCommand}" Cursor="Hand">
<Button.Template>
<ControlTemplate TargetType="{x:Type Button}">
<ContentPresenter />
</ControlTemplate>
</Button.Template>
</Button>
<Label Grid.Row="21" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelPayload}" />
<TextBox Grid.Row="21" Grid.Column="1" Text="{Binding Payload}" AcceptsReturn="True" VerticalScrollBarVisibility="Visible" Height="150" />
</ui:GridLayout>
</UserControl>

View File

@ -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
{
/// <summary>
/// Interaction logic for RawPublisherView.xaml
/// </summary>
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);
}
}
}

View File

@ -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<Header> 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;
}
}

View File

@ -0,0 +1,297 @@
//------------------------------------------------------------------------------
// <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.UI.Tab.Publisher {
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()]
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() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[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;
}
}
/// <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)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Transient (non-persistent).
/// </summary>
public static string DeliveryModeNonPersistent {
get {
return ResourceManager.GetString("DeliveryModeNonPersistent", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Persistent.
/// </summary>
public static string DeliveryModePersistent {
get {
return ResourceManager.GetString("DeliveryModePersistent", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Name.
/// </summary>
public static string HeaderName {
get {
return ResourceManager.GetString("HeaderName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Value.
/// </summary>
public static string HeaderValue {
get {
return ResourceManager.GetString("HeaderValue", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to App ID.
/// </summary>
public static string LabelAppId {
get {
return ResourceManager.GetString("LabelAppId", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Content encoding.
/// </summary>
public static string LabelContentEncoding {
get {
return ResourceManager.GetString("LabelContentEncoding", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Content type.
/// </summary>
public static string LabelContentType {
get {
return ResourceManager.GetString("LabelContentType", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Correlation ID.
/// </summary>
public static string LabelCorrelationId {
get {
return ResourceManager.GetString("LabelCorrelationId", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Delivery mode.
/// </summary>
public static string LabelDeliveryMode {
get {
return ResourceManager.GetString("LabelDeliveryMode", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Exchange.
/// </summary>
public static string LabelExchange {
get {
return ResourceManager.GetString("LabelExchange", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Expiration.
/// </summary>
public static string LabelExpiration {
get {
return ResourceManager.GetString("LabelExpiration", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Headers.
/// </summary>
public static string LabelHeaders {
get {
return ResourceManager.GetString("LabelHeaders", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Message ID.
/// </summary>
public static string LabelMessageId {
get {
return ResourceManager.GetString("LabelMessageId", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Payload.
/// </summary>
public static string LabelPayload {
get {
return ResourceManager.GetString("LabelPayload", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Priority.
/// </summary>
public static string LabelPriority {
get {
return ResourceManager.GetString("LabelPriority", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Properties.
/// </summary>
public static string LabelProperties {
get {
return ResourceManager.GetString("LabelProperties", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Queue.
/// </summary>
public static string LabelQueue {
get {
return ResourceManager.GetString("LabelQueue", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Reply To.
/// </summary>
public static string LabelReplyTo {
get {
return ResourceManager.GetString("LabelReplyTo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Routing key.
/// </summary>
public static string LabelRoutingKey {
get {
return ResourceManager.GetString("LabelRoutingKey", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Publish to exchange (topic).
/// </summary>
public static string LabelSendToExchange {
get {
return ResourceManager.GetString("LabelSendToExchange", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Publish to queue (direct).
/// </summary>
public static string LabelSendToQueue {
get {
return ResourceManager.GetString("LabelSendToQueue", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Timestamp.
/// </summary>
public static string LabelTimestamp {
get {
return ResourceManager.GetString("LabelTimestamp", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Type.
/// </summary>
public static string LabelType {
get {
return ResourceManager.GetString("LabelType", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to User ID.
/// </summary>
public static string LabelUserId {
get {
return ResourceManager.GetString("LabelUserId", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to ⏶ Collapse.
/// </summary>
public static string PropertiesCollapse {
get {
return ResourceManager.GetString("PropertiesCollapse", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to ⏷ Expand.
/// </summary>
public static string PropertiesExpand {
get {
return ResourceManager.GetString("PropertiesExpand", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,198 @@
<?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=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="DeliveryModeNonPersistent" xml:space="preserve">
<value>Transient (non-persistent)</value>
</data>
<data name="DeliveryModePersistent" xml:space="preserve">
<value>Persistent</value>
</data>
<data name="HeaderName" xml:space="preserve">
<value>Name</value>
</data>
<data name="HeaderValue" xml:space="preserve">
<value>Value</value>
</data>
<data name="LabelAppId" xml:space="preserve">
<value>App ID</value>
</data>
<data name="LabelContentEncoding" xml:space="preserve">
<value>Content encoding</value>
</data>
<data name="LabelContentType" xml:space="preserve">
<value>Content type</value>
</data>
<data name="LabelCorrelationId" xml:space="preserve">
<value>Correlation ID</value>
</data>
<data name="LabelDeliveryMode" xml:space="preserve">
<value>Delivery mode</value>
</data>
<data name="LabelExchange" xml:space="preserve">
<value>Exchange</value>
</data>
<data name="LabelExpiration" xml:space="preserve">
<value>Expiration</value>
</data>
<data name="LabelHeaders" xml:space="preserve">
<value>Headers</value>
</data>
<data name="LabelMessageId" xml:space="preserve">
<value>Message ID</value>
</data>
<data name="LabelPayload" xml:space="preserve">
<value>Payload</value>
</data>
<data name="LabelPriority" xml:space="preserve">
<value>Priority</value>
</data>
<data name="LabelProperties" xml:space="preserve">
<value>Properties</value>
</data>
<data name="LabelQueue" xml:space="preserve">
<value>Queue</value>
</data>
<data name="LabelReplyTo" xml:space="preserve">
<value>Reply To</value>
</data>
<data name="LabelRoutingKey" xml:space="preserve">
<value>Routing key</value>
</data>
<data name="LabelSendToExchange" xml:space="preserve">
<value>Publish to exchange (topic)</value>
</data>
<data name="LabelSendToQueue" xml:space="preserve">
<value>Publish to queue (direct)</value>
</data>
<data name="LabelTimestamp" xml:space="preserve">
<value>Timestamp</value>
</data>
<data name="LabelType" xml:space="preserve">
<value>Type</value>
</data>
<data name="LabelUserId" xml:space="preserve">
<value>User ID</value>
</data>
<data name="PropertiesCollapse" xml:space="preserve">
<value>⏶ Collapse</value>
</data>
<data name="PropertiesExpand" xml:space="preserve">
<value>⏷ Expand</value>
</data>
</root>

View File

@ -28,8 +28,8 @@
<ColumnDefinition Width="Auto" MinWidth="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Grid.Row="0" Text="{Binding Timestamp, StringFormat=g}" Style="{StaticResource Timestamp}"></TextBlock>
<TextBlock Grid.Column="1" Grid.Row="0" Text="{Binding RoutingKey}" Style="{StaticResource Routingkey}"></TextBlock>
<TextBlock Grid.Column="0" Grid.Row="0" Text="{Binding ReceivedTimestamp, StringFormat=g}" Style="{StaticResource Timestamp}"></TextBlock>
<TextBlock Grid.Column="1" Grid.Row="0" Text="{Binding RoutingKey}" Style="{StaticResource RoutingKey}"></TextBlock>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>

View File

@ -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<string, string>? selectedMessageProperties;
public ICommand ClearCommand => clearCommand;
public ObservableCollection<MessageInfo> Messages { get; }
public ObservableCollection<ReceivedMessageInfo> 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<string, string>? SelectedMessageProperties => SelectedMessage?.Properties;
public IDictionary<string, string>? SelectedMessageProperties
{
get => selectedMessageProperties;
set => SetField(ref selectedMessageProperties, value);
}
public string Title => $"{subscriber.Exchange} - {subscriber.RoutingKey}";
public IEnumerable<TabToolbarCommand> ToolbarCommands => toolbarCommands;
@ -57,7 +56,7 @@ namespace PettingZoo.UI.Tab.Subscriber
uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
Messages = new ObservableCollection<MessageInfo>();
Messages = new ObservableCollection<ReceivedMessageInfo>();
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<MessageReceivedEventArgs>? MessageReceived;
#pragma warning restore CS0067