1
0
mirror of synced 2024-11-27 05:03:09 +01:00

Fixed #2: Improve connection status and changing servers

This commit is contained in:
Mark van Renswoude 2022-01-30 11:12:35 +01:00
parent d6b9970d51
commit 3d229b5ea8
17 changed files with 350 additions and 85 deletions

View File

@ -17,5 +17,11 @@
Username = username; Username = username;
Password = password; Password = password;
} }
public override string ToString()
{
return $"{Host}:{Port}{VirtualHost}";
}
} }
} }

View File

@ -0,0 +1,100 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
namespace PettingZoo.Core.Connection
{
public class DynamicConnection : IConnection
{
public Guid ConnectionId => currentConnection?.ConnectionId ?? Guid.Empty;
public ConnectionParams? ConnectionParams { get; private set; }
public ConnectionStatus Status { get; private set; } = ConnectionStatus.Disconnected;
public event EventHandler<StatusChangedEventArgs>? StatusChanged;
private IConnection? currentConnection;
public async ValueTask DisposeAsync()
{
if (currentConnection != null)
await currentConnection.DisposeAsync();
GC.SuppressFinalize(this);
}
public void Connect()
{
CheckConnection();
currentConnection.Connect();
}
public async ValueTask Disconnect()
{
if (currentConnection == null)
return;
var disconnectedConnectionId = currentConnection.ConnectionId;
await currentConnection.DisposeAsync();
currentConnection = null;
ConnectionStatusChanged(this, new StatusChangedEventArgs(disconnectedConnectionId, ConnectionStatus.Disconnected));
}
public void SetConnection(IConnection connection)
{
if (currentConnection != null)
{
currentConnection.StatusChanged -= ConnectionStatusChanged;
ConnectionStatusChanged(this, new StatusChangedEventArgs(currentConnection.ConnectionId, ConnectionStatus.Disconnected));
}
currentConnection = connection;
// Assume we get the new connection before Connect is called, thus before the status changes
if (currentConnection != null)
currentConnection.StatusChanged += ConnectionStatusChanged;
}
public ISubscriber Subscribe(string exchange, string routingKey)
{
CheckConnection();
return currentConnection.Subscribe(exchange, routingKey);
}
public ISubscriber Subscribe()
{
CheckConnection();
return currentConnection.Subscribe();
}
public Task Publish(PublishMessageInfo messageInfo)
{
CheckConnection();
return currentConnection.Publish(messageInfo);
}
private void ConnectionStatusChanged(object? sender, StatusChangedEventArgs e)
{
ConnectionParams = e.ConnectionParams;
Status = e.Status;
StatusChanged?.Invoke(sender, e);
}
[MemberNotNull(nameof(currentConnection))]
private void CheckConnection()
{
if (currentConnection == null)
throw new InvalidOperationException("No current connection");
}
}
}

View File

@ -5,8 +5,15 @@ namespace PettingZoo.Core.Connection
{ {
public interface IConnection : IAsyncDisposable public interface IConnection : IAsyncDisposable
{ {
Guid ConnectionId { get; }
ConnectionParams? ConnectionParams { get; }
ConnectionStatus Status { get; }
event EventHandler<StatusChangedEventArgs> StatusChanged; event EventHandler<StatusChangedEventArgs> StatusChanged;
void Connect();
ISubscriber Subscribe(string exchange, string routingKey); ISubscriber Subscribe(string exchange, string routingKey);
ISubscriber Subscribe(); ISubscriber Subscribe();
@ -25,14 +32,18 @@ namespace PettingZoo.Core.Connection
public class StatusChangedEventArgs : EventArgs public class StatusChangedEventArgs : EventArgs
{ {
public Guid ConnectionId { get; }
public ConnectionStatus Status { get; } public ConnectionStatus Status { get; }
public string? Context { get; } public ConnectionParams? ConnectionParams { get; }
public Exception? Exception { get; }
public StatusChangedEventArgs(ConnectionStatus status, string? context) public StatusChangedEventArgs(Guid connectionId, ConnectionStatus status, ConnectionParams? connectionParams = null, Exception? exception = null)
{ {
ConnectionId = connectionId;
Status = status; Status = status;
Context = context; ConnectionParams = connectionParams;
Exception = exception;
} }
} }
} }

View File

@ -3,51 +3,56 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using PettingZoo.Core.Connection; using PettingZoo.Core.Connection;
using RabbitMQ.Client; using RabbitMQ.Client;
using IConnection = RabbitMQ.Client.IConnection;
namespace PettingZoo.RabbitMQ namespace PettingZoo.RabbitMQ
{ {
public class RabbitMQClientConnection : Core.Connection.IConnection public class RabbitMQClientConnection : Core.Connection.IConnection
{ {
public Guid ConnectionId { get; } = Guid.NewGuid();
public ConnectionParams? ConnectionParams { get; }
public ConnectionStatus Status { get; set; }
public event EventHandler<StatusChangedEventArgs>? StatusChanged;
private const int ConnectRetryDelay = 5000; private const int ConnectRetryDelay = 5000;
private readonly CancellationTokenSource connectionTaskToken = new(); private readonly CancellationTokenSource connectionTaskToken = new();
private readonly Task connectionTask; private Task? connectionTask;
private readonly object connectionLock = new(); private readonly object connectionLock = new();
private global::RabbitMQ.Client.IConnection? connection; private IConnection? connection;
private IModel? model;
public event EventHandler<StatusChangedEventArgs>? StatusChanged;
public RabbitMQClientConnection(ConnectionParams connectionParams) public RabbitMQClientConnection(ConnectionParams connectionParams)
{ {
connectionTask = Task.Factory.StartNew(() => TryConnection(connectionParams, connectionTaskToken.Token), CancellationToken.None); ConnectionParams = connectionParams;
} }
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
GC.SuppressFinalize(this);
if (connectionTask == null)
return;
connectionTaskToken.Cancel(); connectionTaskToken.Cancel();
if (!connectionTask.IsCompleted) if (!connectionTask.IsCompleted)
await connectionTask; await connectionTask;
lock (connectionLock) lock (connectionLock)
{ {
if (model != null)
{
model.Dispose();
model = null;
}
if (connection != null) if (connection != null)
{ {
connection.Dispose(); connection.Dispose();
connection = null; connection = null;
} }
} }
}
GC.SuppressFinalize(this);
public void Connect()
{
connectionTask = Task.Factory.StartNew(() => TryConnection(ConnectionParams!, connectionTaskToken.Token), CancellationToken.None);
} }
@ -67,6 +72,7 @@ namespace PettingZoo.RabbitMQ
{ {
lock (connectionLock) lock (connectionLock)
{ {
var model = connection?.CreateModel();
var subscriber = new RabbitMQClientSubscriber(model, exchange, routingKey); var subscriber = new RabbitMQClientSubscriber(model, exchange, routingKey);
if (model != null) if (model != null)
return subscriber; return subscriber;
@ -79,10 +85,10 @@ namespace PettingZoo.RabbitMQ
lock (connectionLock) lock (connectionLock)
{ {
if (model == null) if (connection == null)
return; return;
subscriber.Connected(model); subscriber.Connected(connection.CreateModel());
} }
StatusChanged -= ConnectSubscriber; StatusChanged -= ConnectSubscriber;
@ -97,12 +103,30 @@ namespace PettingZoo.RabbitMQ
public Task Publish(PublishMessageInfo messageInfo) public Task Publish(PublishMessageInfo messageInfo)
{ {
if (model == null) IConnection? lockedConnection;
lock (connectionLock)
{
lockedConnection = connection;
}
if (lockedConnection == null)
throw new InvalidOperationException("Not connected"); throw new InvalidOperationException("Not connected");
model.BasicPublish(messageInfo.Exchange, messageInfo.RoutingKey, false, using (var model = lockedConnection.CreateModel())
RabbitMQClientPropertiesConverter.Convert(messageInfo.Properties, model.CreateBasicProperties()), {
messageInfo.Body); try
{
model.BasicPublish(messageInfo.Exchange, messageInfo.RoutingKey, false,
RabbitMQClientPropertiesConverter.Convert(messageInfo.Properties,
model.CreateBasicProperties()),
messageInfo.Body);
}
finally
{
model.Close();
}
}
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -119,22 +143,22 @@ namespace PettingZoo.RabbitMQ
Password = connectionParams.Password Password = connectionParams.Password
}; };
var statusContext = $"{connectionParams.Host}:{connectionParams.Port}{connectionParams.VirtualHost}";
while (!cancellationToken.IsCancellationRequested) while (!cancellationToken.IsCancellationRequested)
{ {
DoStatusChanged(ConnectionStatus.Connecting, statusContext); DoStatusChanged(ConnectionStatus.Connecting);
try try
{ {
connection = factory.CreateConnection(); lock (connectionLock)
model = connection.CreateModel(); {
connection = factory.CreateConnection();
DoStatusChanged(ConnectionStatus.Connected, statusContext); }
DoStatusChanged(ConnectionStatus.Connected);
break; break;
} }
catch (Exception e) catch (Exception e)
{ {
DoStatusChanged(ConnectionStatus.Error, e.Message); DoStatusChanged(ConnectionStatus.Error, e);
try try
{ {
@ -148,9 +172,10 @@ namespace PettingZoo.RabbitMQ
} }
private void DoStatusChanged(ConnectionStatus status, string? context = null) private void DoStatusChanged(ConnectionStatus status, Exception? exception = null)
{ {
StatusChanged?.Invoke(this, new StatusChangedEventArgs(status, context)); Status = status;
StatusChanged?.Invoke(this, new StatusChangedEventArgs(ConnectionId, status, ConnectionParams, exception));
} }
} }
} }

View File

@ -160,7 +160,7 @@ namespace PettingZoo.UI.Main {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Connected. /// Looks up a localized string similar to Connected to {0}.
/// </summary> /// </summary>
public static string StatusConnected { public static string StatusConnected {
get { get {

View File

@ -151,7 +151,7 @@
<value>Importing messages...</value> <value>Importing messages...</value>
</data> </data>
<data name="StatusConnected" xml:space="preserve"> <data name="StatusConnected" xml:space="preserve">
<value>Connected</value> <value>Connected to {0}</value>
</data> </data>
<data name="StatusConnecting" xml:space="preserve"> <data name="StatusConnecting" xml:space="preserve">
<value>Connecting to {0}...</value> <value>Connecting to {0}...</value>

View File

@ -43,7 +43,7 @@ namespace PettingZoo.UI.Main
private readonly IExportImportFormatProvider exportImportFormatProvider; private readonly IExportImportFormatProvider exportImportFormatProvider;
private SubscribeDialogParams? subscribeDialogParams; private SubscribeDialogParams? subscribeDialogParams;
private IConnection? connection; private readonly DynamicConnection connection = new();
private string connectionStatus; private string connectionStatus;
private ITab? activeTab; private ITab? activeTab;
private readonly Dictionary<ITab, Window> undockedTabs = new(); private readonly Dictionary<ITab, Window> undockedTabs = new();
@ -141,15 +141,15 @@ namespace PettingZoo.UI.Main
closeTabCommand = new DelegateCommand(CloseTabExecute, HasActiveTabCanExecute); closeTabCommand = new DelegateCommand(CloseTabExecute, HasActiveTabCanExecute);
undockTabCommand = new DelegateCommand(UndockTabExecute, HasActiveTabCanExecute); undockTabCommand = new DelegateCommand(UndockTabExecute, HasActiveTabCanExecute);
importCommand = new DelegateCommand(ImportExecute); importCommand = new DelegateCommand(ImportExecute);
connection.StatusChanged += ConnectionStatusChanged;
} }
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
await connection.DisposeAsync();
if (connection != null)
await connection.DisposeAsync();
} }
@ -159,13 +159,9 @@ namespace PettingZoo.UI.Main
if (connectionSettings == null) if (connectionSettings == null)
return; return;
if (connection != null) connection.SetConnection(connectionFactory.CreateConnection(new ConnectionParams(
await connection.DisposeAsync();
connection = connectionFactory.CreateConnection(new ConnectionParams(
connectionSettings.Host, connectionSettings.VirtualHost, connectionSettings.Port, connectionSettings.Host, connectionSettings.VirtualHost, connectionSettings.Port,
connectionSettings.Username, connectionSettings.Password)); connectionSettings.Username, connectionSettings.Password)));
connection.StatusChanged += ConnectionStatusChanged;
if (connectionSettings.Subscribe) if (connectionSettings.Subscribe)
{ {
@ -173,40 +169,22 @@ namespace PettingZoo.UI.Main
tabFactory.CreateSubscriberTab(connection, subscriber); tabFactory.CreateSubscriberTab(connection, subscriber);
} }
connection.Connect();
ConnectionChanged(); ConnectionChanged();
} }
private async void DisconnectExecute() private async void DisconnectExecute()
{ {
Tabs.Clear(); await connection.Disconnect();
var capturedUndockedTabs = undockedTabs.ToList();
undockedTabs.Clear();
foreach (var undockedTab in capturedUndockedTabs)
undockedTab.Value.Close();
RaisePropertyChanged(nameof(NoTabsVisibility));
undockTabCommand.RaiseCanExecuteChanged();
if (connection != null)
{
await connection.DisposeAsync();
connection = null;
}
ConnectionStatus = GetConnectionStatus(null);
ConnectionStatusType = ConnectionStatusType.Error;
ConnectionChanged();
} }
private void SubscribeExecute() private void SubscribeExecute()
{ {
if (connection == null) if (connection.Status != Core.Connection.ConnectionStatus.Connected)
return; return;
var newParams = subscribeDialog.Show(subscribeDialogParams); var newParams = subscribeDialog.Show(subscribeDialogParams);
if (newParams == null) if (newParams == null)
return; return;
@ -220,16 +198,16 @@ namespace PettingZoo.UI.Main
private void PublishExecute() private void PublishExecute()
{ {
if (connection == null) if (connection.Status != Core.Connection.ConnectionStatus.Connected)
return; return;
tabFactory.CreatePublisherTab(connection); tabFactory.CreatePublisherTab(connection);
} }
private bool IsConnectedCanExecute() private bool IsConnectedCanExecute()
{ {
return connection != null; return connection.Status == Core.Connection.ConnectionStatus.Connected;
} }
@ -419,6 +397,8 @@ namespace PettingZoo.UI.Main
Core.Connection.ConnectionStatus.Connecting => ConnectionStatusType.Connecting, Core.Connection.ConnectionStatus.Connecting => ConnectionStatusType.Connecting,
_ => ConnectionStatusType.Error _ => ConnectionStatusType.Error
}; };
Application.Current.Dispatcher.BeginInvoke(ConnectionChanged);
} }
@ -427,9 +407,9 @@ namespace PettingZoo.UI.Main
{ {
return args?.Status switch return args?.Status switch
{ {
Core.Connection.ConnectionStatus.Connecting => string.Format(MainWindowStrings.StatusConnecting, args.Context), Core.Connection.ConnectionStatus.Connecting => string.Format(MainWindowStrings.StatusConnecting, args.ConnectionParams),
Core.Connection.ConnectionStatus.Connected => string.Format(MainWindowStrings.StatusConnected, args.Context), Core.Connection.ConnectionStatus.Connected => string.Format(MainWindowStrings.StatusConnected, args.ConnectionParams),
Core.Connection.ConnectionStatus.Error => string.Format(MainWindowStrings.StatusError, args.Context), Core.Connection.ConnectionStatus.Error => string.Format(MainWindowStrings.StatusError, args.Exception?.Message),
Core.Connection.ConnectionStatus.Disconnected => MainWindowStrings.StatusDisconnected, Core.Connection.ConnectionStatus.Disconnected => MainWindowStrings.StatusDisconnected,
_ => MainWindowStrings.StatusDisconnected _ => MainWindowStrings.StatusDisconnected
}; };

View File

@ -4,7 +4,7 @@ namespace PettingZoo.UI.Tab
{ {
public interface ITabFactory public interface ITabFactory
{ {
void CreateSubscriberTab(IConnection? connection, ISubscriber subscriber); void CreateSubscriberTab(IConnection connection, ISubscriber subscriber);
string CreateReplySubscriberTab(IConnection connection); string CreateReplySubscriberTab(IConnection connection);
void CreatePublisherTab(IConnection connection, ReceivedMessageInfo? fromReceivedMessage = null); void CreatePublisherTab(IConnection connection, ReceivedMessageInfo? fromReceivedMessage = null);
} }

View File

@ -8,7 +8,7 @@
xmlns:svgc="http://sharpvectors.codeplex.com/svgc/" xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
xmlns:valueConverters="clr-namespace:PettingZoo.WPF.ValueConverters;assembly=PettingZoo.WPF" xmlns:valueConverters="clr-namespace:PettingZoo.WPF.ValueConverters;assembly=PettingZoo.WPF"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="450" d:DesignHeight="1200"
d:DesignWidth="800" d:DesignWidth="800"
d:DataContext="{d:DesignInstance res:DesignTimePublisherViewModel, IsDesignTimeCreatable=True}" d:DataContext="{d:DesignInstance res:DesignTimePublisherViewModel, IsDesignTimeCreatable=True}"
Background="White"> Background="White">
@ -34,6 +34,7 @@
<RowDefinition Height="16" /> <RowDefinition Height="16" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Label" /> <ColumnDefinition Width="Auto" SharedSizeGroup="Label" />
@ -81,6 +82,7 @@
<ContentControl Grid.Row="10" Grid.Column="0" Grid.ColumnSpan="2" Margin="0 8 0 0" Content="{Binding MessageTypeControl}" /> <ContentControl Grid.Row="10" Grid.Column="0" Grid.ColumnSpan="2" Margin="0 8 0 0" Content="{Binding MessageTypeControl}" />
<Button Grid.Row="11" Grid.Column="1" Command="{Binding PublishCommand}" Content="{x:Static res:PublisherViewStrings.CommandPublish}" HorizontalAlignment="Left" /> <Button Grid.Row="11" Grid.Column="1" Command="{Binding PublishCommand}" Content="{x:Static res:PublisherViewStrings.CommandPublish}" HorizontalAlignment="Left" />
<TextBlock Grid.Row="12" Grid.Column="1" Text="{x:Static res:PublisherViewStrings.Published}" Visibility="{Binding PublishedVisibility}" />
</controls:GridLayout> </controls:GridLayout>
</ScrollViewer> </ScrollViewer>

View File

@ -8,7 +8,7 @@ using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Forms; using System.Windows.Forms;
using System.Windows.Input; using System.Windows.Input;
using Accessibility; using System.Windows.Threading;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
using PettingZoo.Core.Connection; using PettingZoo.Core.Connection;
@ -17,11 +17,12 @@ using PettingZoo.Core.Generator;
using PettingZoo.Core.Macros; using PettingZoo.Core.Macros;
using PettingZoo.Core.Settings; using PettingZoo.Core.Settings;
using PettingZoo.WPF.ViewModel; using PettingZoo.WPF.ViewModel;
using Application = System.Windows.Application;
using UserControl = System.Windows.Controls.UserControl; using UserControl = System.Windows.Controls.UserControl;
namespace PettingZoo.UI.Tab.Publisher namespace PettingZoo.UI.Tab.Publisher
{ {
public class PublisherViewModel : BaseViewModel, ITabToolbarCommands, ITabHostWindowNotify, IPublishDestination public class PublisherViewModel : BaseViewModel, IDisposable, ITabToolbarCommands, ITabHostWindowNotify, IPublishDestination
{ {
private readonly IConnection connection; private readonly IConnection connection;
private readonly IExampleGenerator exampleGenerator; private readonly IExampleGenerator exampleGenerator;
@ -197,6 +198,19 @@ namespace PettingZoo.UI.Tab.Publisher
public ICommand ImportCommand => importCommand; public ICommand ImportCommand => importCommand;
private readonly DispatcherTimer publishedVisibilityTimer = new()
{
Interval = TimeSpan.FromSeconds(1)
};
private Visibility publishedVisibility = Visibility.Hidden;
public Visibility PublishedVisibility
{
get => publishedVisibility;
set => SetField(ref publishedVisibility, value);
}
public string Title => SendToQueue public string Title => SendToQueue
? string.IsNullOrWhiteSpace(Queue) ? PublisherViewStrings.TabTitleEmpty : ? string.IsNullOrWhiteSpace(Queue) ? PublisherViewStrings.TabTitleEmpty :
string.Format(PublisherViewStrings.TabTitle, Queue) string.Format(PublisherViewStrings.TabTitle, Queue)
@ -244,17 +258,51 @@ namespace PettingZoo.UI.Tab.Publisher
PropertyChanged += (_, _) => { saveCommand.RaiseCanExecuteChanged(); }; PropertyChanged += (_, _) => { saveCommand.RaiseCanExecuteChanged(); };
// ReSharper disable once ConditionIsAlwaysTrueOrFalse - null in design time
if (connection != null)
connection.StatusChanged += ConnectionStatusChanged;
publishedVisibilityTimer.Tick += (_, _) =>
{
PublishedVisibility = Visibility.Hidden;
publishedVisibilityTimer.Stop();
};
} }
public void Dispose()
{
connection.StatusChanged -= ConnectionStatusChanged;
GC.SuppressFinalize(this);
}
private void ConnectionStatusChanged(object? sender, StatusChangedEventArgs e)
{
Application.Current.Dispatcher.BeginInvoke(() =>
{
publishCommand.RaiseCanExecuteChanged();
});
}
private void PublishExecute() private void PublishExecute()
{ {
messageTypePublishCommand?.Execute(null); messageTypePublishCommand?.Execute(null);
PublishedVisibility = Visibility.Visible;
publishedVisibilityTimer.Stop();
publishedVisibilityTimer.Start();
} }
private bool PublishCanExecute() private bool PublishCanExecute()
{ {
if (connection.Status != ConnectionStatus.Connected)
return false;
if (SendToExchange) if (SendToExchange)
{ {
if (string.IsNullOrWhiteSpace(Exchange) || string.IsNullOrWhiteSpace(RoutingKey)) if (string.IsNullOrWhiteSpace(Exchange) || string.IsNullOrWhiteSpace(RoutingKey))
@ -571,6 +619,8 @@ namespace PettingZoo.UI.Tab.Publisher
SelectedStoredMessage = StoredMessages[0]; SelectedStoredMessage = StoredMessages[0];
ActiveStoredMessage = StoredMessages[1]; ActiveStoredMessage = StoredMessages[1];
}; };
PublishedVisibility = Visibility.Visible;
} }
public override Visibility ExchangeVisibility => Visibility.Visible; public override Visibility ExchangeVisibility => Visibility.Visible;

View File

@ -177,6 +177,15 @@ namespace PettingZoo.UI.Tab.Publisher {
} }
} }
/// <summary>
/// Looks up a localized string similar to Message published.
/// </summary>
public static string Published {
get {
return ResourceManager.GetString("Published", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Re: . /// Looks up a localized string similar to Re: .
/// </summary> /// </summary>

View File

@ -156,6 +156,9 @@
<data name="PanelTitleMessages" xml:space="preserve"> <data name="PanelTitleMessages" xml:space="preserve">
<value>Saved messages</value> <value>Saved messages</value>
</data> </data>
<data name="Published" xml:space="preserve">
<value>Message published</value>
</data>
<data name="ReplyToCorrelationIdPrefix" xml:space="preserve"> <data name="ReplyToCorrelationIdPrefix" xml:space="preserve">
<value>Re: </value> <value>Re: </value>
</data> </data>

View File

@ -37,8 +37,8 @@
<ListBox.ItemsSource> <ListBox.ItemsSource>
<CompositeCollection> <CompositeCollection>
<CollectionContainer Collection="{Binding Source={StaticResource Messages}}" /> <CollectionContainer Collection="{Binding Source={StaticResource Messages}}" />
<ListBoxItem HorizontalContentAlignment="Stretch" IsEnabled="False" IsHitTestVisible="False"> <ListBoxItem HorizontalContentAlignment="Stretch" IsEnabled="False" IsHitTestVisible="False" Visibility="{Binding UnreadMessagesVisibility}">
<Grid Visibility="{Binding UnreadMessagesVisibility}"> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="DateTime" /> <ColumnDefinition SharedSizeGroup="DateTime" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
@ -51,6 +51,17 @@
</Grid> </Grid>
</ListBoxItem> </ListBoxItem>
<CollectionContainer Collection="{Binding Source={StaticResource UnreadMessages}}" /> <CollectionContainer Collection="{Binding Source={StaticResource UnreadMessages}}" />
<ListBoxItem HorizontalContentAlignment="Stretch" IsEnabled="False" IsHitTestVisible="False" Visibility="{Binding StatusVisibility}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="DateTime" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="1" Text="{Binding StatusText}" HorizontalAlignment="Center" Background="{Binding Background, RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Foreground="{x:Static SystemColors.GrayTextBrush}" />
</Grid>
</ListBoxItem>
</CompositeCollection> </CompositeCollection>
</ListBox.ItemsSource> </ListBox.ItemsSource>
<ListBox.ItemTemplate> <ListBox.ItemTemplate>

View File

@ -25,7 +25,7 @@ namespace PettingZoo.UI.Tab.Subscriber
{ {
private readonly ILogger logger; private readonly ILogger logger;
private readonly ITabFactory tabFactory; private readonly ITabFactory tabFactory;
private readonly IConnection? connection; private readonly IConnection connection;
private readonly ISubscriber subscriber; private readonly ISubscriber subscriber;
private readonly IExportImportFormatProvider exportImportFormatProvider; private readonly IExportImportFormatProvider exportImportFormatProvider;
private ReceivedMessageInfo? selectedMessage; private ReceivedMessageInfo? selectedMessage;
@ -106,8 +106,14 @@ namespace PettingZoo.UI.Tab.Subscriber
public Visibility ReplyTabVisibility => IsReplyTab ? Visibility.Visible : Visibility.Collapsed; public Visibility ReplyTabVisibility => IsReplyTab ? Visibility.Visible : Visibility.Collapsed;
// ReSharper restore UnusedMember.Global // ReSharper restore UnusedMember.Global
private readonly Guid subscribeConnectionId;
private ConnectionStatus connectionStatus = ConnectionStatus.Connecting;
public SubscriberViewModel(ILogger logger, ITabFactory tabFactory, IConnection? connection, ISubscriber subscriber, IExportImportFormatProvider exportImportFormatProvider, bool isReplyTab) public Visibility StatusVisibility => connectionStatus is ConnectionStatus.Connecting or ConnectionStatus.Disconnected or ConnectionStatus.Error ? Visibility.Visible : Visibility.Collapsed;
public string StatusText => connectionStatus == ConnectionStatus.Connecting ? SubscriberViewStrings.StatusConnecting : SubscriberViewStrings.StatusDisconnected;
public SubscriberViewModel(ILogger logger, ITabFactory tabFactory, IConnection connection, ISubscriber subscriber, IExportImportFormatProvider exportImportFormatProvider, bool isReplyTab)
{ {
IsReplyTab = isReplyTab; IsReplyTab = isReplyTab;
@ -133,6 +139,8 @@ namespace PettingZoo.UI.Tab.Subscriber
createPublisherCommand = new DelegateCommand(CreatePublisherExecute, CreatePublisherCanExecute); createPublisherCommand = new DelegateCommand(CreatePublisherExecute, CreatePublisherCanExecute);
subscribeConnectionId = connection.ConnectionId;
connection.StatusChanged += ConnectionStatusChanged;
subscriber.MessageReceived += SubscriberMessageReceived; subscriber.MessageReceived += SubscriberMessageReceived;
subscriber.Start(); subscriber.Start();
} }
@ -141,11 +149,47 @@ namespace PettingZoo.UI.Tab.Subscriber
public void Dispose() public void Dispose()
{ {
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
connection.StatusChanged -= ConnectionStatusChanged;
newMessageTimer?.Dispose(); newMessageTimer?.Dispose();
subscriber.Dispose(); subscriber.Dispose();
} }
private void ConnectionStatusChanged(object? sender, StatusChangedEventArgs e)
{
// If another connection has been made, this does not concern us
if (e.ConnectionId != subscribeConnectionId)
return;
// The subscriber will not reconnect, so after the first disconnect it's over for us
if (connectionStatus is ConnectionStatus.Disconnected)
return;
switch (e.Status)
{
case ConnectionStatus.Connecting:
case ConnectionStatus.Error:
return;
case ConnectionStatus.Connected:
connectionStatus = ConnectionStatus.Connected;
break;
case ConnectionStatus.Disconnected:
default:
connectionStatus = ConnectionStatus.Disconnected;
break;
}
Application.Current.Dispatcher.BeginInvoke(() =>
{
RaisePropertyChanged(nameof(StatusVisibility));
RaisePropertyChanged(nameof(StatusText));
});
}
private void ClearExecute() private void ClearExecute()
{ {
Messages.Clear(); Messages.Clear();
@ -254,7 +298,7 @@ namespace PettingZoo.UI.Tab.Subscriber
private void CreatePublisherExecute() private void CreatePublisherExecute()
{ {
if (connection == null) if (connection.Status != ConnectionStatus.Connected)
return; return;
tabFactory.CreatePublisherTab(connection, SelectedMessage); tabFactory.CreatePublisherTab(connection, SelectedMessage);
@ -263,7 +307,7 @@ namespace PettingZoo.UI.Tab.Subscriber
private bool CreatePublisherCanExecute() private bool CreatePublisherCanExecute()
{ {
return connection != null && SelectedMessage != null; return SelectedMessage != null;
} }

View File

@ -194,5 +194,23 @@ namespace PettingZoo.UI.Tab.Subscriber {
return ResourceManager.GetString("ReplyTabTitle", resourceCulture); return ResourceManager.GetString("ReplyTabTitle", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Connecting....
/// </summary>
public static string StatusConnecting {
get {
return ResourceManager.GetString("StatusConnecting", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Disconnected.
/// </summary>
public static string StatusDisconnected {
get {
return ResourceManager.GetString("StatusDisconnected", resourceCulture);
}
}
} }
} }

View File

@ -162,4 +162,10 @@
<data name="ReplyTabTitle" xml:space="preserve"> <data name="ReplyTabTitle" xml:space="preserve">
<value>Replies</value> <value>Replies</value>
</data> </data>
<data name="StatusConnecting" xml:space="preserve">
<value>Connecting...</value>
</data>
<data name="StatusDisconnected" xml:space="preserve">
<value>Disconnected</value>
</data>
</root> </root>

View File

@ -36,7 +36,7 @@ namespace PettingZoo.UI.Tab
} }
public void CreateSubscriberTab(IConnection? connection, ISubscriber subscriber) public void CreateSubscriberTab(IConnection connection, ISubscriber subscriber)
{ {
InternalCreateSubscriberTab(connection, subscriber, false); InternalCreateSubscriberTab(connection, subscriber, false);
} }
@ -75,7 +75,7 @@ namespace PettingZoo.UI.Tab
} }
private ITab InternalCreateSubscriberTab(IConnection? connection, ISubscriber subscriber, bool isReplyTab) private ITab InternalCreateSubscriberTab(IConnection connection, ISubscriber subscriber, bool isReplyTab)
{ {
var viewModel = new SubscriberViewModel(logger, this, connection, subscriber, exportImportFormatProvider, isReplyTab); var viewModel = new SubscriberViewModel(logger, this, connection, subscriber, exportImportFormatProvider, isReplyTab);
var tab = new ViewTab<SubscriberView, SubscriberViewModel>( var tab = new ViewTab<SubscriberView, SubscriberViewModel>(