Use single subscriber tab for replies

This commit is contained in:
Mark van Renswoude 2022-01-12 20:54:30 +01:00
parent 6e8029b552
commit 76d4c8fa85
22 changed files with 216 additions and 98 deletions

View File

@ -3,7 +3,7 @@ using System.Collections.Generic;
namespace PettingZoo.Core.Connection
{
public interface ISubscriber : IAsyncDisposable
public interface ISubscriber : IDisposable
{
string? QueueName { get; }
string? Exchange {get; }

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using PettingZoo.Core.Connection;
namespace PettingZoo.Core.ExportImport
@ -13,7 +12,10 @@ namespace PettingZoo.Core.ExportImport
public string? QueueName { get; }
public string? Exchange => null;
public string? RoutingKey => null;
#pragma warning disable CS0067 // "The event ... is never used" - it's part of the interface so it's required.
public event EventHandler<MessageReceivedEventArgs>? MessageReceived;
#pragma warning restore CS0067
public ImportSubscriber(string filename, IReadOnlyList<ReceivedMessageInfo> messages)
@ -23,10 +25,9 @@ namespace PettingZoo.Core.ExportImport
}
public ValueTask DisposeAsync()
public void Dispose()
{
GC.SuppressFinalize(this);
return default;
}

View File

@ -58,7 +58,7 @@ namespace PettingZoo.Core.ExportImport
public StreamWrapper(StreamProgressDecorator owner, Stream decoratedStream)
{
this.owner = owner;
this.DecoratedStream = decoratedStream;
DecoratedStream = decoratedStream;
}

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using PettingZoo.Core.Connection;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
@ -29,14 +28,12 @@ namespace PettingZoo.RabbitMQ
}
public ValueTask DisposeAsync()
public void Dispose()
{
GC.SuppressFinalize(this);
if (model != null && consumerTag != null && model.IsOpen)
model.BasicCancelNoWait(consumerTag);
return default;
}

View File

@ -5,7 +5,6 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using PettingZoo.Core.Generator;
using PettingZoo.Core.Settings;
using PettingZoo.Tapeti.AssemblyLoader;

View File

@ -4,9 +4,6 @@
Should-have
-----------
- Single tab for responses, don't create a new subscriber tab for 1 message each time
Use the CorrelationId in the list for such cases instead of the routing key (which is the name of the dynamic queue for the responses).
Set the CorrelationId to the request routing key for example, so the different responses can be somewhat identified.
Nice-to-have

View File

@ -170,7 +170,7 @@ namespace PettingZoo.UI.Main
if (connectionSettings.Subscribe)
{
var subscriber = connection.Subscribe(connectionSettings.Exchange, connectionSettings.RoutingKey);
AddTab(tabFactory.CreateSubscriberTab(connection, subscriber));
tabFactory.CreateSubscriberTab(connection, subscriber);
}
ConnectionChanged();
@ -214,7 +214,7 @@ namespace PettingZoo.UI.Main
subscribeDialogParams = newParams;
var subscriber = connection.Subscribe(subscribeDialogParams.Exchange, subscribeDialogParams.RoutingKey);
AddTab(tabFactory.CreateSubscriberTab(connection, subscriber));
tabFactory.CreateSubscriberTab(connection, subscriber);
}
@ -223,7 +223,7 @@ namespace PettingZoo.UI.Main
if (connection == null)
return;
AddTab(tabFactory.CreatePublisherTab(connection));
tabFactory.CreatePublisherTab(connection);
}
@ -235,7 +235,8 @@ namespace PettingZoo.UI.Main
private void CloseTabExecute()
{
RemoveActiveTab();
var tab = RemoveActiveTab();
(tab as IDisposable)?.Dispose();
}
@ -300,8 +301,7 @@ namespace PettingZoo.UI.Main
progressWindow.Close();
progressWindow = null;
AddTab(tabFactory.CreateSubscriberTab(connection,
new ImportSubscriber(filename, messages)));
tabFactory.CreateSubscriberTab(connection, new ImportSubscriber(filename, messages));
});
}
catch (OperationCanceledException)
@ -377,6 +377,15 @@ namespace PettingZoo.UI.Main
}
public void ActivateTab(ITab tab)
{
if (undockedTabs.TryGetValue(tab, out var window))
window.Activate();
else if (Tabs.Contains(tab))
ActiveTab = tab;
}
public void DockTab(ITab tab)
{
if (undockedTabs.Remove(tab, out var tabHostWindow))

View File

@ -2,13 +2,10 @@
namespace PettingZoo.UI.Tab
{
// Passing the closeTabCommand is necessary because I haven't figured out how to bind the main window's
// context menu items for the tab to the main window's datacontext yet. RelativeSource doesn't seem to work
// because the popup is it's own window. Refactor if a better solution is found.
public interface ITabFactory
{
ITab CreateSubscriberTab(IConnection? connection, ISubscriber subscriber);
ITab CreatePublisherTab(IConnection connection, ReceivedMessageInfo? fromReceivedMessage = null);
void CreateSubscriberTab(IConnection? connection, ISubscriber subscriber);
string CreateReplySubscriberTab(IConnection connection);
void CreatePublisherTab(IConnection connection, ReceivedMessageInfo? fromReceivedMessage = null);
}
}

View File

@ -3,6 +3,7 @@
public interface ITabHost
{
void AddTab(ITab tab);
void ActivateTab(ITab tab);
void DockTab(ITab tab);
void UndockedTabClosed(ITab tab);

View File

@ -5,6 +5,6 @@
string Exchange { get; }
string RoutingKey { get; }
string? GetReplyTo();
string? GetReplyTo(ref string? correlationId);
}
}

View File

@ -21,7 +21,6 @@ namespace PettingZoo.UI.Tab.Publisher
private readonly IConnection connection;
private readonly IExampleGenerator exampleGenerator;
private readonly ITabFactory tabFactory;
private readonly ITabHostProvider tabHostProvider;
private bool sendToExchange = true;
private string exchange = "";
@ -156,12 +155,11 @@ namespace PettingZoo.UI.Tab.Publisher
string IPublishDestination.RoutingKey => SendToExchange ? RoutingKey : Queue;
public PublisherViewModel(ITabHostProvider tabHostProvider, ITabFactory tabFactory, IConnection connection, IExampleGenerator exampleGenerator, ReceivedMessageInfo? fromReceivedMessage = null)
public PublisherViewModel(ITabFactory tabFactory, IConnection connection, IExampleGenerator exampleGenerator, ReceivedMessageInfo? fromReceivedMessage = null)
{
this.connection = connection;
this.exampleGenerator = exampleGenerator;
this.tabFactory = tabFactory;
this.tabHostProvider = tabHostProvider;
publishCommand = new DelegateCommand(PublishExecute, PublishCanExecute);
@ -280,17 +278,13 @@ namespace PettingZoo.UI.Tab.Publisher
}
public string? GetReplyTo()
public string? GetReplyTo(ref string? correlationId)
{
if (ReplyToSpecified)
return string.IsNullOrEmpty(ReplyTo) ? null : ReplyTo;
var subscriber = connection.Subscribe();
var tab = tabFactory.CreateSubscriberTab(connection, subscriber);
tabHostProvider.Instance.AddTab(tab);
subscriber.Start();
return subscriber.QueueName;
correlationId = SendToExchange ? RoutingKey : Queue;
return tabFactory.CreateReplySubscriberTab(connection);
}
@ -305,7 +299,7 @@ namespace PettingZoo.UI.Tab.Publisher
public class DesignTimePublisherViewModel : PublisherViewModel
{
public DesignTimePublisherViewModel() : base(null!, null!, null!, null!)
public DesignTimePublisherViewModel() : base(null!, null!, null!)
{
}

View File

@ -244,6 +244,8 @@ namespace PettingZoo.UI.Tab.Publisher
var headers = Headers.Where(h => h.IsValid()).ToDictionary(h => h.Key, h => h.Value);
var publishCorrelationId = NullIfEmpty(CorrelationId);
var replyTo = publishDestination.GetReplyTo(ref publishCorrelationId);
connection.Publish(new PublishMessageInfo(
publishDestination.Exchange,
@ -254,12 +256,12 @@ namespace PettingZoo.UI.Tab.Publisher
AppId = NullIfEmpty(AppId),
ContentEncoding = NullIfEmpty(ContentEncoding),
ContentType = NullIfEmpty(ContentType),
CorrelationId = NullIfEmpty(CorrelationId),
CorrelationId = publishCorrelationId,
DeliveryMode = deliveryMode,
Expiration = NullIfEmpty(Expiration),
MessageId = NullIfEmpty(MessageId),
Priority = priorityValue,
ReplyTo = publishDestination.GetReplyTo(),
ReplyTo = replyTo,
Timestamp = timestampValue,
Type = NullIfEmpty(TypeProperty),
UserId = NullIfEmpty(UserId)

View File

@ -2,7 +2,6 @@
using System.Text;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using PettingZoo.Core.Connection;
using PettingZoo.Core.Generator;
using PettingZoo.Core.Validation;
@ -151,7 +150,10 @@ namespace PettingZoo.UI.Tab.Publisher
{
return string.IsNullOrEmpty(value) ? null : value;
}
var publishCorrelationId = NullIfEmpty(CorrelationId);
var replyTo = publishDestination.GetReplyTo(ref publishCorrelationId);
connection.Publish(new PublishMessageInfo(
publishDestination.Exchange,
publishDestination.RoutingKey,
@ -162,9 +164,9 @@ namespace PettingZoo.UI.Tab.Publisher
})
{
ContentType = @"application/json",
CorrelationId = NullIfEmpty(CorrelationId),
CorrelationId = publishCorrelationId,
DeliveryMode = MessageDeliveryMode.Persistent,
ReplyTo = publishDestination.GetReplyTo()
ReplyTo = replyTo
}));
}

View File

@ -1,23 +0,0 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace PettingZoo.UI.Tab.Subscriber
{
public class SameMessageVisibilityConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return ReferenceEquals(values[0], values[1])
? Visibility.Visible
: Visibility.Collapsed;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
}

View File

@ -13,9 +13,6 @@
d:DesignWidth="800"
d:DataContext="{d:DesignInstance res:DesignTimeSubscriberViewModel, IsDesignTimeCreatable=True}"
Background="White">
<UserControl.Resources>
<res:SameMessageVisibilityConverter x:Key="SameMessageVisibilityConverter" />
</UserControl.Resources>
<Grid Margin="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
@ -64,7 +61,10 @@
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding ReceivedTimestamp, StringFormat=g}" Style="{StaticResource Timestamp}" />
<TextBlock Grid.Column="1" Text="{Binding RoutingKey}" Style="{StaticResource RoutingKey}" />
<!-- ReSharper disable Xaml.BindingWithContextNotResolved -->
<TextBlock Grid.Column="1" Text="{Binding RoutingKey}" Style="{StaticResource RoutingKey}" Visibility="{Binding Data.StandardTabVisibility, Source={StaticResource ContextMenuProxy}}" />
<TextBlock Grid.Column="1" Text="{Binding Properties.CorrelationId}" Style="{StaticResource RoutingKey}" Visibility="{Binding Data.ReplyToTabVisibility, Source={StaticResource ContextMenuProxy}}" />
<!-- ReSharper restore Xaml.BindingWithContextNotResolved -->
<Grid.ContextMenu>
<ContextMenu>

View File

@ -21,10 +21,9 @@ using Timer = System.Threading.Timer;
namespace PettingZoo.UI.Tab.Subscriber
{
public class SubscriberViewModel : BaseViewModel, ITabToolbarCommands, ITabActivate
public class SubscriberViewModel : BaseViewModel, IDisposable, ITabToolbarCommands, ITabActivate
{
private readonly ILogger logger;
private readonly ITabHostProvider tabHostProvider;
private readonly ITabFactory tabFactory;
private readonly IConnection? connection;
private readonly ISubscriber subscriber;
@ -76,16 +75,43 @@ namespace PettingZoo.UI.Tab.Subscriber
set => SetField(ref selectedMessageProperties, value);
}
public string Title =>
(subscriber.Exchange != null ? $"{subscriber.Exchange} - {subscriber.RoutingKey}" : $"{subscriber.QueueName}") +
(tabActive || unreadCount == 0 ? "" : $" ({unreadCount})");
public string Title
{
get
{
var title = new StringBuilder();
if (IsReplyTab)
title.Append(SubscriberViewStrings.ReplyTabTitle);
else if (subscriber.Exchange != null)
title.Append(subscriber.Exchange).Append(" - ").Append(subscriber.RoutingKey);
else
title.Append(subscriber.QueueName);
if (!tabActive && unreadCount > 0)
title.Append(" (").Append(unreadCount).Append(')');
return title.ToString();
}
}
public IEnumerable<TabToolbarCommand> ToolbarCommands => toolbarCommands;
public SubscriberViewModel(ILogger logger, ITabHostProvider tabHostProvider, ITabFactory tabFactory, IConnection? connection, ISubscriber subscriber, IExportImportFormatProvider exportImportFormatProvider)
public bool IsReplyTab { get; }
// ReSharper disable UnusedMember.Global - used via BindingProxy
public Visibility StandardTabVisibility => !IsReplyTab ? Visibility.Visible : Visibility.Collapsed;
public Visibility ReplyTabVisibility => IsReplyTab ? Visibility.Visible : Visibility.Collapsed;
// ReSharper restore UnusedMember.Global
public SubscriberViewModel(ILogger logger, ITabFactory tabFactory, IConnection? connection, ISubscriber subscriber, IExportImportFormatProvider exportImportFormatProvider, bool isReplyTab)
{
IsReplyTab = isReplyTab;
this.logger = logger;
this.tabHostProvider = tabHostProvider;
this.tabFactory = tabFactory;
this.connection = connection;
this.subscriber = subscriber;
@ -111,6 +137,15 @@ namespace PettingZoo.UI.Tab.Subscriber
subscriber.Start();
}
public void Dispose()
{
GC.SuppressFinalize(this);
newMessageTimer?.Dispose();
subscriber.Dispose();
}
private void ClearExecute()
{
Messages.Clear();
@ -222,8 +257,7 @@ namespace PettingZoo.UI.Tab.Subscriber
if (connection == null)
return;
var publisherTab = tabFactory.CreatePublisherTab(connection, SelectedMessage);
tabHostProvider.Instance.AddTab(publisherTab);
tabFactory.CreatePublisherTab(connection, SelectedMessage);
}
@ -320,7 +354,7 @@ namespace PettingZoo.UI.Tab.Subscriber
public class DesignTimeSubscriberViewModel : SubscriberViewModel
{
public DesignTimeSubscriberViewModel() : base(null!, null!, null!, null!, new DesignTimeSubscriber(), null!)
public DesignTimeSubscriberViewModel() : base(null!, null!, null!, new DesignTimeSubscriber(), null!, false)
{
for (var i = 1; i <= 5; i++)
(i > 2 ? UnreadMessages : Messages).Add(new ReceivedMessageInfo(
@ -340,9 +374,9 @@ namespace PettingZoo.UI.Tab.Subscriber
private class DesignTimeSubscriber : ISubscriber
{
public ValueTask DisposeAsync()
public void Dispose()
{
return default;
GC.SuppressFinalize(this);
}

View File

@ -185,5 +185,14 @@ namespace PettingZoo.UI.Tab.Subscriber {
return ResourceManager.GetString("PropertyValue", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Replies.
/// </summary>
public static string ReplyTabTitle {
get {
return ResourceManager.GetString("ReplyTabTitle", resourceCulture);
}
}
}
}

View File

@ -159,4 +159,7 @@
<data name="PropertyValue" xml:space="preserve">
<value>Value</value>
</data>
<data name="ReplyTabTitle" xml:space="preserve">
<value>Replies</value>
</data>
</root>

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
@ -13,6 +14,7 @@ namespace PettingZoo.UI.Tab.Undocked
private readonly ITabHostProvider tabHostProvider;
private readonly ITab tab;
private readonly DelegateCommand dockCommand;
private bool docked;
public string Title => tab.Title;
@ -43,13 +45,18 @@ namespace PettingZoo.UI.Tab.Undocked
private void DockCommandExecute()
{
docked = true;
tabHostProvider.Instance.DockTab(tab);
}
public void WindowClosed()
{
if (docked)
return;
tabHostProvider.Instance.UndockedTabClosed(tab);
(tab as IDisposable)?.Dispose();
}

View File

@ -1,7 +1,5 @@
using System;
using System.Windows;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
namespace PettingZoo.UI.Tab.Undocked
{

View File

@ -8,7 +8,7 @@ using System.Windows.Controls;
namespace PettingZoo.UI.Tab
{
public class ViewTab<TView, TViewModel> : ITab, ITabToolbarCommands, ITabActivate, ITabHostWindowNotify where TView : ContentControl where TViewModel : INotifyPropertyChanged
public class ViewTab<TView, TViewModel> : IDisposable, ITab, ITabToolbarCommands, ITabActivate, ITabHostWindowNotify where TView : ContentControl where TViewModel : INotifyPropertyChanged
{
public string Title => getTitle(viewModel);
public ContentControl Content { get; }
@ -63,5 +63,12 @@ namespace PettingZoo.UI.Tab
{
(viewModel as ITabHostWindowNotify)?.HostWindowChanged(hostWindow);
}
public void Dispose()
{
GC.SuppressFinalize(this);
(viewModel as IDisposable)?.Dispose();
}
}
}

View File

@ -1,4 +1,6 @@
using PettingZoo.Core.Connection;
using System;
using System.Collections.Generic;
using PettingZoo.Core.Connection;
using PettingZoo.Core.ExportImport;
using PettingZoo.Core.Generator;
using PettingZoo.UI.Tab.Publisher;
@ -14,6 +16,10 @@ namespace PettingZoo.UI.Tab
private readonly IExampleGenerator exampleGenerator;
private readonly IExportImportFormatProvider exportImportFormatProvider;
// Not the cleanest way, but this factory itself can't be singleton without (justifyable) upsetting SimpleInjector
private static ISubscriber? replySubscriber;
private static ITab? replySubscriberTab;
public ViewTabFactory(ILogger logger, ITabHostProvider tabHostProvider, IExampleGenerator exampleGenerator, IExportImportFormatProvider exportImportFormatProvider)
{
@ -24,23 +30,101 @@ namespace PettingZoo.UI.Tab
}
public ITab CreateSubscriberTab(IConnection? connection, ISubscriber subscriber)
public void CreateSubscriberTab(IConnection? connection, ISubscriber subscriber)
{
var viewModel = new SubscriberViewModel(logger, tabHostProvider, this, connection, subscriber, exportImportFormatProvider);
return new ViewTab<SubscriberView, SubscriberViewModel>(
new SubscriberView(viewModel),
viewModel,
vm => vm.Title);
InternalCreateSubscriberTab(connection, subscriber, false);
}
public ITab CreatePublisherTab(IConnection connection, ReceivedMessageInfo? fromReceivedMessage = null)
public string CreateReplySubscriberTab(IConnection connection)
{
var viewModel = new PublisherViewModel(tabHostProvider, this, connection, exampleGenerator, fromReceivedMessage);
return new ViewTab<PublisherView, PublisherViewModel>(
if (replySubscriber?.QueueName != null && replySubscriberTab != null)
{
tabHostProvider.Instance.ActivateTab(replySubscriberTab);
return replySubscriber.QueueName;
}
replySubscriber = new SubscriberDecorator(connection.Subscribe(), () =>
{
replySubscriber = null;
replySubscriberTab = null;
});
replySubscriber.Start();
replySubscriberTab = InternalCreateSubscriberTab(connection, replySubscriber, true);
return replySubscriber.QueueName!;
}
public void CreatePublisherTab(IConnection connection, ReceivedMessageInfo? fromReceivedMessage = null)
{
var viewModel = new PublisherViewModel(this, connection, exampleGenerator, fromReceivedMessage);
var tab = new ViewTab<PublisherView, PublisherViewModel>(
new PublisherView(viewModel),
viewModel,
vm => vm.Title);
tabHostProvider.Instance.AddTab(tab);
}
private ITab InternalCreateSubscriberTab(IConnection? connection, ISubscriber subscriber, bool isReplyTab)
{
var viewModel = new SubscriberViewModel(logger, this, connection, subscriber, exportImportFormatProvider, isReplyTab);
var tab = new ViewTab<SubscriberView, SubscriberViewModel>(
new SubscriberView(viewModel),
viewModel,
vm => vm.Title);
tabHostProvider.Instance.AddTab(tab);
return tab;
}
private class SubscriberDecorator : ISubscriber
{
private readonly ISubscriber decoratedSubscriber;
private readonly Action onDispose;
public string? QueueName => decoratedSubscriber.QueueName;
public string? Exchange => decoratedSubscriber.Exchange;
public string? RoutingKey => decoratedSubscriber.RoutingKey;
public event EventHandler<MessageReceivedEventArgs>? MessageReceived;
public SubscriberDecorator(ISubscriber decoratedSubscriber, Action onDispose)
{
this.decoratedSubscriber = decoratedSubscriber;
this.onDispose = onDispose;
decoratedSubscriber.MessageReceived += (sender, args) =>
{
MessageReceived?.Invoke(sender, args);
};
}
public void Dispose()
{
GC.SuppressFinalize(this);
decoratedSubscriber.Dispose();
onDispose();
}
public IEnumerable<ReceivedMessageInfo> GetInitialMessages()
{
return decoratedSubscriber.GetInitialMessages();
}
public void Start()
{
decoratedSubscriber.Start();
}
}
}
}